apache-iggy 0.10.0-edge.7 → 0.10.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/README.md CHANGED
@@ -19,9 +19,11 @@ note: previous works on node.js http client has been moved to [iggy-node-http-cl
19
19
  ## install
20
20
 
21
21
  ```bash
22
- npm i --save apache-iggy
22
+ npm i --save apache-iggy@edge
23
23
  ```
24
24
 
25
+ Use the `edge` package with server 0.9.0 or `edge`. Stable Node SDK 0.8.0 uses the older wire protocol. For a local server with the example credentials, follow the [example setup](../../examples/node/README.md#running-examples).
26
+
25
27
  ## basic usage
26
28
 
27
29
  ### Response frame limit
@@ -49,12 +51,9 @@ Codes absent from the SDK command table use `Operation::NonReplicated` and
49
51
  carry the command code in the request header's reserved field. The server
50
52
  remains authoritative for classifying or rejecting extension commands.
51
53
 
52
- Sends must use explicit `Partitioning.PartitionId` partitioning: the client
53
- routes each request to a partition-scoped namespace, so broker-side balancing
54
- (`Partitioning.Balanced`) and key hashing (`Partitioning.MessageKey`) are
55
- rejected before the request is sent.
56
- <!-- TODO(hubcio): Balanced and MessageKey partitioning to be implemented;
57
- not decided yet whether it'll be on server side or client side. -->
54
+ Sends encode `Partitioning.PartitionId`, `Partitioning.Balanced` or
55
+ `Partitioning.MessageKey` in the payload. The server resolves the target
56
+ partition at admission.
58
57
 
59
58
  VSR works over TCP and TLS. It restricts `Client` to one pooled connection because authentication, request sequencing, and consumer-group assignments belong to one consensus session. Configurations requesting more than one pooled connection fail before a socket is opened.
60
59
 
@@ -69,8 +68,9 @@ The client pings every `heartbeatInterval` milliseconds, 5000 by default, which
69
68
  keeps an idle session alive when the server's `[heartbeat]` eviction is enabled.
70
69
  `heartbeatInterval` also accepts a duration expression such as `"10s"` or
71
70
  `"1h 30m"`, like the Rust SDK.
72
- The server evicts a connection silent for 36 s, which is 1.2 x its 30 s
73
- heartbeat interval. Raising the client interval past that window, or setting it
71
+ With the default heartbeat settings, a group member becomes eligible for
72
+ eviction after 36 s of silence (1.2 x the 30 s interval); the verifier checks
73
+ once per interval. Raising the client interval past that window, or setting it
74
74
  to 0 to disable client heartbeats, exposes an idle consumer-group member to
75
75
  eviction; a connection holding no group membership is left alone. Any other
76
76
  unusable value is rejected instead of silently disabling the heartbeat.
@@ -146,6 +146,13 @@ Durations accept the same expressions as the Rust SDK, for example `500ms`,
146
146
 
147
147
  ## use sources
148
148
 
149
+ Cluster auto-commit polling over TCP/TLS keeps group membership on the coordinator
150
+ and uses separate connections to partition primaries. It requires server support
151
+ for binary commands 14, 103 and 104. Pause binary auto-commit consumers for the
152
+ whole upgrade: upgrade every server first, then the SDKs, and restart consumers
153
+ so they rejoin their groups. Older SDKs can lose membership when a backup refuses
154
+ an offset commit; the new SDK does not fall back to legacy polling.
155
+
149
156
  ### Install
150
157
 
151
158
  ```bash
@@ -68,7 +68,7 @@ export declare class IggyConnection extends EventEmitter {
68
68
  *
69
69
  * @returns Promise that resolves when connected
70
70
  */
71
- connect(): Promise<this>;
71
+ connect(boundDial?: boolean): Promise<this>;
72
72
  /**
73
73
  * Waits for one dial, bounded while other endpoints are queued behind it.
74
74
  *
@@ -229,7 +229,7 @@ export class IggyConnection extends EventEmitter {
229
229
  *
230
230
  * @returns Promise that resolves when connected
231
231
  */
232
- connect() {
232
+ connect(boundDial = false) {
233
233
  if (this.ending)
234
234
  return Promise.reject(new Error('connection is closed'));
235
235
  if (this.connected)
@@ -245,7 +245,7 @@ export class IggyConnection extends EventEmitter {
245
245
  // Bounded here too when the client knows somewhere else to go: this dial
246
246
  // is not part of a pass, so an endpoint that never becomes usable would
247
247
  // hold it with no timer of its own and the redial pass would never start.
248
- const connectPromise = this._dialWithin(socket, this._redialCandidates().length > 1);
248
+ const connectPromise = this._dialWithin(socket, boundDial || this._redialCandidates().length > 1);
249
249
  this.connectPromise = connectPromise;
250
250
  const clearConnectPromise = () => {
251
251
  if (this.connectPromise === connectPromise)
@@ -54,6 +54,12 @@ export declare class CommandResponseStream extends EventEmitter {
54
54
  heartbeatIntervalHandler?: NodeJS.Timeout;
55
55
  /** Whether a heartbeat ping is still awaiting its response */
56
56
  private heartbeatInFlight;
57
+ private rememberedCredentials?;
58
+ private clustered?;
59
+ private metadataWatermark;
60
+ private routingGeneration;
61
+ private pollRoutes;
62
+ private pollConnections;
57
63
  /**
58
64
  * Creates a new CommandResponseStream.
59
65
  *
@@ -109,6 +115,12 @@ export declare class CommandResponseStream extends EventEmitter {
109
115
  */
110
116
  private _releaseUndecidedMove;
111
117
  private _rememberRoster;
118
+ private _pollOnPrimary;
119
+ private _pollRoutingControl;
120
+ private _observeMetadataReply;
121
+ private _rememberCredentials;
122
+ private _signInCredentials;
123
+ private _clearPollRouting;
112
124
  /**
113
125
  * Processes queued commands sequentially.
114
126
  * Emits 'finishQueue' when all commands are processed.
@@ -20,8 +20,12 @@ import { debug } from './client.debug.js';
20
20
  import { endpointKey, IggyConnection } from './client.connection.js';
21
21
  import { LOGIN, LOGIN_WITH_TOKEN, LOGOUT, PING } from '../wire/index.js';
22
22
  import { GET_CLUSTER_METADATA } from '../wire/cluster/get-cluster-metadata.command.js';
23
+ import { deserializeNode } from '../wire/cluster/cluster.utils.js';
23
24
  import { COMMAND_CODE } from '../wire/command.code.js';
24
- import { decodeVsrResponse, prepareVsrCommand, readRegisteredSession, VsrEvictionError, VsrSession, } from '../wire/vsr/index.js';
25
+ import { serializeIdentifier } from '../wire/identifier.utils.js';
26
+ import { Command, HEADER_SIZE, REPLY_OFFSET, peekCommand, readReplyOperation, readStatus } from '../wire/vsr/header.js';
27
+ import { Operation, isKnownOperation } from '../wire/vsr/operation.js';
28
+ import { decodeVsrResponse, prepareVsrCommand, readRegisteredSession, readWireName, VsrEvictionError, VsrSession, } from '../wire/vsr/index.js';
25
29
  import { normalizeClientConfig } from './client.config.js';
26
30
  const VSR_RESPONSE_TIMEOUT_MS = 30_000;
27
31
  const VSR_RETRY_INTERVAL_MS = 50;
@@ -30,6 +34,14 @@ const LEADERLESS_POLL_INTERVAL_MS = 250;
30
34
  const MAX_LEADER_REDIRECTS = 3;
31
35
  const TRANSIENT_NOT_COMMITTED = 57;
32
36
  const TRANSIENT_NOT_ACCEPTED = 58;
37
+ const UNAUTHENTICATED = 40;
38
+ const STALE_CLIENT = 30;
39
+ const FEATURE_UNAVAILABLE = 5;
40
+ const MAX_POLL_ROUTES = 4096;
41
+ const MAX_POLL_CONNECTIONS = 256;
42
+ const CONSUMER_SESSION_SIZE = 32;
43
+ const METADATA_WATERMARK_OFFSET = 24;
44
+ const POLL_OPTIONS_SIZE = 9 + 4 + 1;
33
45
  /**
34
46
  * How long a `TRANSIENT_NOT_ACCEPTED` request replays on the same connection
35
47
  * before the roster is re-read. A node that stopped being primary refuses
@@ -121,6 +133,12 @@ export class CommandResponseStream extends EventEmitter {
121
133
  heartbeatIntervalHandler;
122
134
  /** Whether a heartbeat ping is still awaiting its response */
123
135
  heartbeatInFlight;
136
+ rememberedCredentials;
137
+ clustered;
138
+ metadataWatermark = 0n;
139
+ routingGeneration = 0;
140
+ pollRoutes = new Map();
141
+ pollConnections = new Map();
124
142
  /**
125
143
  * Creates a new CommandResponseStream.
126
144
  *
@@ -209,10 +227,26 @@ export class CommandResponseStream extends EventEmitter {
209
227
  this.pendingSubmissions += 1;
210
228
  try {
211
229
  const { handleResponse = true, last = true, followsLeaderMoves = true } = options;
230
+ const autoCommitPoll = command === COMMAND_CODE.PollMessages &&
231
+ payload.length > POLL_OPTIONS_SIZE && payload.at(-1) === 1;
232
+ const pollDeadline = autoCommitPoll
233
+ ? options.deadline ?? Date.now() + VSR_RESPONSE_TIMEOUT_MS
234
+ : undefined;
212
235
  if (!this.connection.connected)
213
- await this.connection.connect();
236
+ await (pollDeadline === undefined ? this.connection.connect()
237
+ : withinDeadline(this.connection.connect(), pollDeadline));
214
238
  if (!this.isAuthenticated && !this.isUnloggedCommand(command))
215
- await this.authenticate(this.options.credentials);
239
+ await (pollDeadline === undefined ? this.authenticate(this._signInCredentials())
240
+ : withinDeadline(this.authenticate(this._signInCredentials()), pollDeadline));
241
+ if (pollDeadline !== undefined) {
242
+ const deadline = pollDeadline;
243
+ if (this.clustered === undefined)
244
+ await withinDeadline(this.sendCommand(GET_CLUSTER_METADATA.code, GET_CLUSTER_METADATA.serialize(), { deadline, followsLeaderMoves: false }), deadline);
245
+ if (this.clustered === undefined)
246
+ throw new Error('cannot determine the poll routing topology');
247
+ if (this.clustered)
248
+ return await this._pollOnPrimary(payload, deadline, handleResponse);
249
+ }
216
250
  // The roster read is itself a queued command and the queue is
217
251
  // single-flighted, so the leader re-check cannot happen inside
218
252
  // `_processVsr`. The refusal comes back out here instead, where the
@@ -289,7 +323,7 @@ export class CommandResponseStream extends EventEmitter {
289
323
  // command fails client-side, a non-replicated one goes out with
290
324
  // session 0.
291
325
  if (!this.isAuthenticated && !this.isUnloggedCommand(command))
292
- await this.authenticate(this.options.credentials);
326
+ await this.authenticate(this._signInCredentials());
293
327
  }
294
328
  }
295
329
  if (!isLoginCommand(command) || this.settlingLeader)
@@ -424,6 +458,9 @@ export class CommandResponseStream extends EventEmitter {
424
458
  _rememberRoster(response) {
425
459
  try {
426
460
  const metadata = GET_CLUSTER_METADATA.deserialize(response);
461
+ if (metadata.nodes.length === 0)
462
+ return;
463
+ this.clustered = metadata.nodes.length > 1;
427
464
  this.connection.rememberRoster(metadata.nodes
428
465
  .filter((node) => node.endpoints.tcp !== 0)
429
466
  .map((node) => ({ host: node.ip, port: node.endpoints.tcp })));
@@ -432,6 +469,201 @@ export class CommandResponseStream extends EventEmitter {
432
469
  debug('an unreadable roster leaves the redial candidates as they are', error);
433
470
  }
434
471
  }
472
+ async _pollOnPrimary(payload, deadline, handleResponse) {
473
+ const key = payload.subarray(0, -POLL_OPTIONS_SIZE).toString('hex');
474
+ while (Date.now() < deadline && !this.connection.ending) {
475
+ try {
476
+ const generation = this.routingGeneration;
477
+ let route = this.pollRoutes.get(key);
478
+ if (!route ||
479
+ route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
480
+ this.metadataWatermark) {
481
+ const response = await withinDeadline(this._pollRoutingControl(payload, deadline), deadline);
482
+ if (generation !== this.routingGeneration)
483
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
484
+ if (response.data.length < CONSUMER_SESSION_SIZE)
485
+ throw new Error('poll routing response is incomplete');
486
+ const node = deserializeNode(response.data, CONSUMER_SESSION_SIZE);
487
+ if (node.length + CONSUMER_SESSION_SIZE !== response.data.length || !node.data.ip)
488
+ throw new Error('poll routing response has no valid TCP endpoint');
489
+ if (node.data.endpoints.tcp === 0)
490
+ throw responseError(COMMAND_CODE.PollMessages, FEATURE_UNAVAILABLE);
491
+ const attachment = Buffer.from(response.data.subarray(0, CONSUMER_SESSION_SIZE));
492
+ if (attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) < this.metadataWatermark)
493
+ attachment.writeBigUInt64LE(this.metadataWatermark, METADATA_WATERMARK_OFFSET);
494
+ route = {
495
+ endpoint: { host: node.data.ip, port: node.data.endpoints.tcp },
496
+ attachment
497
+ };
498
+ if (this.pollRoutes.size >= MAX_POLL_ROUTES)
499
+ this.pollRoutes.clear();
500
+ this.pollRoutes.set(key, route);
501
+ }
502
+ const endpoint = endpointKey(route.endpoint);
503
+ let entry = this.pollConnections.get(endpoint);
504
+ if (!entry) {
505
+ if (this.pollConnections.size >= MAX_POLL_CONNECTIONS) {
506
+ const idle = Array.from(this.pollConnections).find(([, value]) => !value.busy);
507
+ if (!idle)
508
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
509
+ this.pollConnections.delete(idle[0]);
510
+ idle[1].client.destroy();
511
+ }
512
+ entry = {
513
+ client: new CommandResponseStream({
514
+ ...this.options,
515
+ options: { ...this.options.options, ...route.endpoint },
516
+ heartbeatInterval: 0,
517
+ reconnect: { enabled: false, interval: 0, maxRetries: 0 }
518
+ })
519
+ };
520
+ this.pollConnections.set(endpoint, entry);
521
+ }
522
+ while (entry.busy)
523
+ await withinDeadline(entry.busy, deadline);
524
+ if (generation !== this.routingGeneration ||
525
+ this.pollConnections.get(endpoint) !== entry ||
526
+ route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
527
+ this.metadataWatermark)
528
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
529
+ let release;
530
+ entry.busy = new Promise((resolve) => { release = resolve; });
531
+ let polling = false;
532
+ try {
533
+ if (!entry.client.isAuthenticated) {
534
+ entry.attachment = undefined;
535
+ await withinDeadline(entry.client.connection.connect(true), deadline, () => entry.client.destroy());
536
+ const credentials = this._signInCredentials();
537
+ const login = 'token' in credentials ? LOGIN_WITH_TOKEN : LOGIN;
538
+ const loginPayload = 'token' in credentials
539
+ ? LOGIN_WITH_TOKEN.serialize(credentials)
540
+ : LOGIN.serialize(credentials);
541
+ await entry.client._queueCommand(login.code, loginPayload, true, true, false, deadline);
542
+ }
543
+ if (!entry.attachment?.equals(route.attachment)) {
544
+ await entry.client._queueCommand(COMMAND_CODE.AttachConsumerSession, route.attachment, true, true, false, deadline);
545
+ entry.attachment = route.attachment;
546
+ }
547
+ if (generation !== this.routingGeneration ||
548
+ route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
549
+ this.metadataWatermark)
550
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
551
+ polling = true;
552
+ return await entry.client._queueCommand(COMMAND_CODE.PollMessagesOnPrimary, payload, handleResponse, true, false, deadline);
553
+ }
554
+ catch (error) {
555
+ if (error instanceof ResponseError &&
556
+ error.errorCode === TRANSIENT_NOT_ACCEPTED) {
557
+ entry.attachment = undefined;
558
+ }
559
+ else {
560
+ if (this.pollConnections.get(endpoint) === entry)
561
+ this.pollConnections.delete(endpoint);
562
+ entry.client.destroy();
563
+ }
564
+ if (!(error instanceof ResponseError) || error instanceof VsrEvictionError ||
565
+ error.errorCode === UNAUTHENTICATED || error.errorCode === STALE_CLIENT)
566
+ throw responseError(COMMAND_CODE.PollMessages, polling ? TRANSIENT_NOT_COMMITTED : TRANSIENT_NOT_ACCEPTED);
567
+ throw error;
568
+ }
569
+ finally {
570
+ entry.busy = undefined;
571
+ release();
572
+ }
573
+ }
574
+ catch (error) {
575
+ this.pollRoutes.delete(key);
576
+ if (error instanceof VsrResponseTimeoutError)
577
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_COMMITTED);
578
+ if (!(error instanceof ResponseError) ||
579
+ error.errorCode !== TRANSIENT_NOT_ACCEPTED)
580
+ throw error instanceof ResponseError
581
+ ? responseError(COMMAND_CODE.PollMessages, error.errorCode)
582
+ : error;
583
+ if (!worthAnotherAttempt(deadline))
584
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
585
+ await delay(VSR_RETRY_INTERVAL_MS);
586
+ }
587
+ }
588
+ throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
589
+ }
590
+ async _pollRoutingControl(payload, deadline) {
591
+ const options = { deadline, followsLeaderMoves: false };
592
+ try {
593
+ return await this.sendCommand(COMMAND_CODE.GetPollRouting, payload, options);
594
+ }
595
+ catch (error) {
596
+ if (error instanceof ResponseError &&
597
+ error.errorCode !== UNAUTHENTICATED && error.errorCode !== STALE_CLIENT)
598
+ throw error;
599
+ if (Date.now() >= deadline)
600
+ throw error;
601
+ // Only a failed coordinator exchange uses normal session recovery.
602
+ // A healthy coordinator refusing a route must retain its group membership.
603
+ if (error instanceof ResponseError) {
604
+ this.connection.abort();
605
+ this.connection.connected = false;
606
+ this._resetSession();
607
+ }
608
+ await this.sendCommand(PING.code, PING.serialize(), { deadline });
609
+ return this.sendCommand(COMMAND_CODE.GetPollRouting, payload, options);
610
+ }
611
+ }
612
+ _observeMetadataReply(frame) {
613
+ if (frame.length < HEADER_SIZE || peekCommand(frame) !== Command.Reply)
614
+ return;
615
+ const operation = readReplyOperation(frame);
616
+ if (!isKnownOperation(operation) || operation === Operation.NonReplicated ||
617
+ operation === Operation.SendMessages || operation === Operation.StoreConsumerOffset ||
618
+ operation === Operation.DeleteConsumerOffset)
619
+ return;
620
+ const commit = frame.readBigUInt64LE(REPLY_OFFSET.commit);
621
+ if (commit > this.metadataWatermark)
622
+ this.metadataWatermark = commit;
623
+ }
624
+ _rememberCredentials(command, payload) {
625
+ if (command === LOGIN.code) {
626
+ const username = readWireName(payload, 0);
627
+ this.rememberedCredentials = {
628
+ username: username.value, password: readWireName(payload, username.next).value
629
+ };
630
+ }
631
+ else if (command === LOGIN_WITH_TOKEN.code) {
632
+ this.rememberedCredentials = { token: readWireName(payload, 0).value };
633
+ }
634
+ else if ((command === COMMAND_CODE.ChangePassword || command === COMMAND_CODE.UpdateUser) &&
635
+ this.userId !== undefined && this.rememberedCredentials &&
636
+ 'username' in this.rememberedCredentials) {
637
+ const credentials = this.rememberedCredentials;
638
+ const identifier = [serializeIdentifier(this.userId), serializeIdentifier(credentials.username)]
639
+ .find((encoded) => payload.subarray(0, encoded.length).equals(encoded));
640
+ if (!identifier)
641
+ return;
642
+ if (command === COMMAND_CODE.ChangePassword) {
643
+ const currentPassword = readWireName(payload, identifier.length);
644
+ const password = readWireName(payload, currentPassword.next).value;
645
+ this.rememberedCredentials = { ...credentials, password };
646
+ if ('username' in this.options.credentials && this.options.credentials.username === credentials.username)
647
+ this.options.credentials = { ...this.options.credentials, password };
648
+ }
649
+ else if (payload[identifier.length] === 1) {
650
+ const username = readWireName(payload, identifier.length + 1).value;
651
+ this.rememberedCredentials = { ...credentials, username };
652
+ if ('username' in this.options.credentials && this.options.credentials.username === credentials.username)
653
+ this.options.credentials = { ...this.options.credentials, username };
654
+ }
655
+ }
656
+ }
657
+ _signInCredentials() {
658
+ return this.rememberedCredentials ?? this.options.credentials;
659
+ }
660
+ _clearPollRouting() {
661
+ this.routingGeneration += 1;
662
+ this.pollRoutes.clear();
663
+ for (const entry of this.pollConnections.values())
664
+ entry.client.destroy();
665
+ this.pollConnections.clear();
666
+ }
435
667
  /**
436
668
  * Processes queued commands sequentially.
437
669
  * Emits 'finishQueue' when all commands are processed.
@@ -535,8 +767,15 @@ export class CommandResponseStream extends EventEmitter {
535
767
  finally {
536
768
  requestWritten ||= exchangeState.written;
537
769
  }
538
- if (!handleResp)
770
+ this._observeMetadataReply(response);
771
+ if (!handleResp) {
772
+ // Routing still needs refusals when the caller decodes the frame.
773
+ if (command === COMMAND_CODE.PollMessagesOnPrimary &&
774
+ peekCommand(response) === Command.Reply &&
775
+ readStatus(response) === TRANSIENT_NOT_ACCEPTED)
776
+ throw responseError(command, TRANSIENT_NOT_ACCEPTED);
539
777
  return response;
778
+ }
540
779
  try {
541
780
  parsed = decodeVsrResponse(response, command);
542
781
  break;
@@ -545,6 +784,10 @@ export class CommandResponseStream extends EventEmitter {
545
784
  if (!(error instanceof ResponseError) ||
546
785
  !isTransientVsrError(error.errorCode))
547
786
  throw error;
787
+ if (command === COMMAND_CODE.PollMessagesOnPrimary ||
788
+ command === COMMAND_CODE.GetPollRouting ||
789
+ command === COMMAND_CODE.AttachConsumerSession)
790
+ throw error;
548
791
  lastTransientError = error;
549
792
  // A not-admitted refusal is a statement about who leads, not about
550
793
  // load: a node that stopped being primary refuses forever, so
@@ -568,7 +811,9 @@ export class CommandResponseStream extends EventEmitter {
568
811
  this.isAuthenticated = true;
569
812
  this.userId = parsed.data.readUInt32LE(0);
570
813
  }
814
+ this._rememberCredentials(command, payload);
571
815
  if (prepared.command === COMMAND_CODE.LogoutUser) {
816
+ this.rememberedCredentials = undefined;
572
817
  this._resetSession();
573
818
  }
574
819
  // Every roster read feeds the redial candidates, whoever asked for it
@@ -735,6 +980,8 @@ export class CommandResponseStream extends EventEmitter {
735
980
  this._execQueue = [];
736
981
  }
737
982
  _resetSession() {
983
+ this._clearPollRouting();
984
+ this.clustered = undefined;
738
985
  if (!this.isAuthenticated &&
739
986
  this.userId === undefined &&
740
987
  !this.vsrSession.hasActivity)
@@ -855,6 +1102,8 @@ export class CommandResponseStream extends EventEmitter {
855
1102
  * Stops heartbeat and destroys the connection.
856
1103
  */
857
1104
  destroy() {
1105
+ this.rememberedCredentials = undefined;
1106
+ this._clearPollRouting();
858
1107
  if (this.heartbeatIntervalHandler)
859
1108
  clearInterval(this.heartbeatIntervalHandler);
860
1109
  return this.connection._destroy();
@@ -873,6 +1122,23 @@ export function getRawClient(options) {
873
1122
  const isLoginCommand = (command) => command === COMMAND_CODE.LoginUser ||
874
1123
  command === COMMAND_CODE.LoginWithAccessToken;
875
1124
  const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
1125
+ const withinDeadline = async (pending, deadline, expired) => {
1126
+ let timer;
1127
+ try {
1128
+ return await Promise.race([
1129
+ pending,
1130
+ new Promise((_resolve, reject) => {
1131
+ timer = setTimeout(() => {
1132
+ expired?.();
1133
+ reject(new VsrResponseTimeoutError(VSR_RESPONSE_TIMEOUT_MS));
1134
+ }, Math.max(0, deadline - Date.now()));
1135
+ })
1136
+ ]);
1137
+ }
1138
+ finally {
1139
+ clearTimeout(timer);
1140
+ }
1141
+ };
876
1142
  const isTransientVsrError = (errorCode) => errorCode === TRANSIENT_NOT_COMMITTED ||
877
1143
  errorCode === TRANSIENT_NOT_ACCEPTED;
878
1144
  //# sourceMappingURL=client.socket.js.map