UNPKG

28.1 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.Subchannel = exports.subchannelAddressToString = exports.subchannelAddressEqual = exports.isTcpSubchannelAddress = void 0;
20const http2 = require("http2");
21const tls_1 = require("tls");
22const channel_1 = require("./channel");
23const backoff_timeout_1 = require("./backoff-timeout");
24const resolver_1 = require("./resolver");
25const logging = require("./logging");
26const constants_1 = require("./constants");
27const http_proxy_1 = require("./http_proxy");
28const net = require("net");
29const uri_parser_1 = require("./uri-parser");
30const clientVersion = require('../../package.json').version;
31const TRACER_NAME = 'subchannel';
32function trace(text) {
33 logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, text);
34}
35function refTrace(text) {
36 logging.trace(constants_1.LogVerbosity.DEBUG, 'subchannel_refcount', text);
37}
38const MIN_CONNECT_TIMEOUT_MS = 20000;
39const INITIAL_BACKOFF_MS = 1000;
40const BACKOFF_MULTIPLIER = 1.6;
41const MAX_BACKOFF_MS = 120000;
42const BACKOFF_JITTER = 0.2;
43/* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't
44 * have a constant for the max signed 32 bit integer, so this is a simple way
45 * to calculate it */
46const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
47const KEEPALIVE_TIMEOUT_MS = 20000;
48const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_TE, HTTP2_HEADER_USER_AGENT, } = http2.constants;
49/**
50 * Get a number uniformly at random in the range [min, max)
51 * @param min
52 * @param max
53 */
54function uniformRandom(min, max) {
55 return Math.random() * (max - min) + min;
56}
57const tooManyPingsData = Buffer.from('too_many_pings', 'ascii');
58function isTcpSubchannelAddress(address) {
59 return 'port' in address;
60}
61exports.isTcpSubchannelAddress = isTcpSubchannelAddress;
62function subchannelAddressEqual(address1, address2) {
63 if (isTcpSubchannelAddress(address1)) {
64 return (isTcpSubchannelAddress(address2) &&
65 address1.host === address2.host &&
66 address1.port === address2.port);
67 }
68 else {
69 return !isTcpSubchannelAddress(address2) && address1.path === address2.path;
70 }
71}
72exports.subchannelAddressEqual = subchannelAddressEqual;
73function subchannelAddressToString(address) {
74 if (isTcpSubchannelAddress(address)) {
75 return address.host + ':' + address.port;
76 }
77 else {
78 return address.path;
79 }
80}
81exports.subchannelAddressToString = subchannelAddressToString;
82class Subchannel {
83 /**
84 * A class representing a connection to a single backend.
85 * @param channelTarget The target string for the channel as a whole
86 * @param subchannelAddress The address for the backend that this subchannel
87 * will connect to
88 * @param options The channel options, plus any specific subchannel options
89 * for this subchannel
90 * @param credentials The channel credentials used to establish this
91 * connection
92 */
93 constructor(channelTarget, subchannelAddress, options, credentials) {
94 this.channelTarget = channelTarget;
95 this.subchannelAddress = subchannelAddress;
96 this.options = options;
97 this.credentials = credentials;
98 /**
99 * The subchannel's current connectivity state. Invariant: `session` === `null`
100 * if and only if `connectivityState` is IDLE or TRANSIENT_FAILURE.
101 */
102 this.connectivityState = channel_1.ConnectivityState.IDLE;
103 /**
104 * The underlying http2 session used to make requests.
105 */
106 this.session = null;
107 /**
108 * Indicates that the subchannel should transition from TRANSIENT_FAILURE to
109 * CONNECTING instead of IDLE when the backoff timeout ends.
110 */
111 this.continueConnecting = false;
112 /**
113 * A list of listener functions that will be called whenever the connectivity
114 * state changes. Will be modified by `addConnectivityStateListener` and
115 * `removeConnectivityStateListener`
116 */
117 this.stateListeners = [];
118 /**
119 * A list of listener functions that will be called when the underlying
120 * socket disconnects. Used for ending active calls with an UNAVAILABLE
121 * status.
122 */
123 this.disconnectListeners = [];
124 /**
125 * The amount of time in between sending pings
126 */
127 this.keepaliveTimeMs = KEEPALIVE_MAX_TIME_MS;
128 /**
129 * The amount of time to wait for an acknowledgement after sending a ping
130 */
131 this.keepaliveTimeoutMs = KEEPALIVE_TIMEOUT_MS;
132 /**
133 * Indicates whether keepalive pings should be sent without any active calls
134 */
135 this.keepaliveWithoutCalls = false;
136 /**
137 * Tracks calls with references to this subchannel
138 */
139 this.callRefcount = 0;
140 /**
141 * Tracks channels and subchannel pools with references to this subchannel
142 */
143 this.refcount = 0;
144 // Build user-agent string.
145 this.userAgent = [
146 options['grpc.primary_user_agent'],
147 `grpc-node-js/${clientVersion}`,
148 options['grpc.secondary_user_agent'],
149 ]
150 .filter((e) => e)
151 .join(' '); // remove falsey values first
152 if ('grpc.keepalive_time_ms' in options) {
153 this.keepaliveTimeMs = options['grpc.keepalive_time_ms'];
154 }
155 if ('grpc.keepalive_timeout_ms' in options) {
156 this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms'];
157 }
158 if ('grpc.keepalive_permit_without_calls' in options) {
159 this.keepaliveWithoutCalls = options['grpc.keepalive_permit_without_calls'] === 1;
160 }
161 else {
162 this.keepaliveWithoutCalls = false;
163 }
164 this.keepaliveIntervalId = setTimeout(() => { }, 0);
165 clearTimeout(this.keepaliveIntervalId);
166 this.keepaliveTimeoutId = setTimeout(() => { }, 0);
167 clearTimeout(this.keepaliveTimeoutId);
168 const backoffOptions = {
169 initialDelay: options['grpc.initial_reconnect_backoff_ms'],
170 maxDelay: options['grpc.max_reconnect_backoff_ms'],
171 };
172 this.backoffTimeout = new backoff_timeout_1.BackoffTimeout(() => {
173 this.handleBackoffTimer();
174 }, backoffOptions);
175 this.subchannelAddressString = subchannelAddressToString(subchannelAddress);
176 }
177 handleBackoffTimer() {
178 if (this.continueConnecting) {
179 this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.CONNECTING);
180 }
181 else {
182 this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.IDLE);
183 }
184 }
185 /**
186 * Start a backoff timer with the current nextBackoff timeout
187 */
188 startBackoff() {
189 this.backoffTimeout.runOnce();
190 }
191 stopBackoff() {
192 this.backoffTimeout.stop();
193 this.backoffTimeout.reset();
194 }
195 sendPing() {
196 var _a, _b;
197 logging.trace(constants_1.LogVerbosity.DEBUG, 'keepalive', 'Sending ping to ' + this.subchannelAddressString);
198 this.keepaliveTimeoutId = setTimeout(() => {
199 this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
200 }, this.keepaliveTimeoutMs);
201 (_b = (_a = this.keepaliveTimeoutId).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
202 this.session.ping((err, duration, payload) => {
203 clearTimeout(this.keepaliveTimeoutId);
204 });
205 }
206 startKeepalivePings() {
207 var _a, _b;
208 this.keepaliveIntervalId = setInterval(() => {
209 this.sendPing();
210 }, this.keepaliveTimeMs);
211 (_b = (_a = this.keepaliveIntervalId).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
212 /* Don't send a ping immediately because whatever caused us to start
213 * sending pings should also involve some network activity. */
214 }
215 stopKeepalivePings() {
216 clearInterval(this.keepaliveIntervalId);
217 clearTimeout(this.keepaliveTimeoutId);
218 }
219 createSession(proxyConnectionResult) {
220 var _a, _b, _c;
221 if (proxyConnectionResult.realTarget) {
222 trace(this.subchannelAddressString + ' creating HTTP/2 session through proxy to ' + proxyConnectionResult.realTarget);
223 }
224 else {
225 trace(this.subchannelAddressString + ' creating HTTP/2 session');
226 }
227 const targetAuthority = resolver_1.getDefaultAuthority((_a = proxyConnectionResult.realTarget) !== null && _a !== void 0 ? _a : this.channelTarget);
228 let connectionOptions = this.credentials._getConnectionOptions() || {};
229 connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER;
230 if ('grpc-node.max_session_memory' in this.options) {
231 connectionOptions.maxSessionMemory = this.options['grpc-node.max_session_memory'];
232 }
233 let addressScheme = 'http://';
234 if ('secureContext' in connectionOptions) {
235 addressScheme = 'https://';
236 // If provided, the value of grpc.ssl_target_name_override should be used
237 // to override the target hostname when checking server identity.
238 // This option is used for testing only.
239 if (this.options['grpc.ssl_target_name_override']) {
240 const sslTargetNameOverride = this.options['grpc.ssl_target_name_override'];
241 connectionOptions.checkServerIdentity = (host, cert) => {
242 return tls_1.checkServerIdentity(sslTargetNameOverride, cert);
243 };
244 connectionOptions.servername = sslTargetNameOverride;
245 }
246 else {
247 const authorityHostname = (_c = (_b = uri_parser_1.splitHostPort(targetAuthority)) === null || _b === void 0 ? void 0 : _b.host) !== null && _c !== void 0 ? _c : 'localhost';
248 // We want to always set servername to support SNI
249 connectionOptions.servername = authorityHostname;
250 }
251 if (proxyConnectionResult.socket) {
252 /* This is part of the workaround for
253 * https://github.com/nodejs/node/issues/32922. Without that bug,
254 * proxyConnectionResult.socket would always be a plaintext socket and
255 * this would say
256 * connectionOptions.socket = proxyConnectionResult.socket; */
257 connectionOptions.createConnection = (authority, option) => {
258 return proxyConnectionResult.socket;
259 };
260 }
261 }
262 else {
263 /* In all but the most recent versions of Node, http2.connect does not use
264 * the options when establishing plaintext connections, so we need to
265 * establish that connection explicitly. */
266 connectionOptions.createConnection = (authority, option) => {
267 if (proxyConnectionResult.socket) {
268 return proxyConnectionResult.socket;
269 }
270 else {
271 /* net.NetConnectOpts is declared in a way that is more restrictive
272 * than what net.connect will actually accept, so we use the type
273 * assertion to work around that. */
274 return net.connect(this.subchannelAddress);
275 }
276 };
277 }
278 connectionOptions = Object.assign(Object.assign({}, connectionOptions), this.subchannelAddress);
279 /* http2.connect uses the options here:
280 * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036
281 * The spread operator overides earlier values with later ones, so any port
282 * or host values in the options will be used rather than any values extracted
283 * from the first argument. In addition, the path overrides the host and port,
284 * as documented for plaintext connections here:
285 * https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
286 * and for TLS connections here:
287 * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In
288 * earlier versions of Node, http2.connect passes these options to
289 * tls.connect but not net.connect, so in the insecure case we still need
290 * to set the createConnection option above to create the connection
291 * explicitly. We cannot do that in the TLS case because http2.connect
292 * passes necessary additional options to tls.connect.
293 * The first argument just needs to be parseable as a URL and the scheme
294 * determines whether the connection will be established over TLS or not.
295 */
296 const session = http2.connect(addressScheme + targetAuthority, connectionOptions);
297 this.session = session;
298 session.unref();
299 /* For all of these events, check if the session at the time of the event
300 * is the same one currently attached to this subchannel, to ensure that
301 * old events from previous connection attempts cannot cause invalid state
302 * transitions. */
303 session.once('connect', () => {
304 if (this.session === session) {
305 this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.READY);
306 }
307 });
308 session.once('close', () => {
309 if (this.session === session) {
310 trace(this.subchannelAddressString + ' connection closed');
311 this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.TRANSIENT_FAILURE);
312 /* Transitioning directly to IDLE here should be OK because we are not
313 * doing any backoff, because a connection was established at some
314 * point */
315 this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
316 }
317 });
318 session.once('goaway', (errorCode, lastStreamID, opaqueData) => {
319 if (this.session === session) {
320 /* See the last paragraph of
321 * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */
322 if (errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM &&
323 opaqueData.equals(tooManyPingsData)) {
324 this.keepaliveTimeMs = Math.min(2 * this.keepaliveTimeMs, KEEPALIVE_MAX_TIME_MS);
325 logging.log(constants_1.LogVerbosity.ERROR, `Connection to ${uri_parser_1.uriToString(this.channelTarget)} at ${this.subchannelAddressString} rejected by server because of excess pings. Increasing ping interval to ${this.keepaliveTimeMs} ms`);
326 }
327 trace(this.subchannelAddressString +
328 ' connection closed by GOAWAY with code ' +
329 errorCode);
330 this.transitionToState([channel_1.ConnectivityState.CONNECTING, channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
331 }
332 });
333 session.once('error', (error) => {
334 /* Do nothing here. Any error should also trigger a close event, which is
335 * where we want to handle that. */
336 trace(this.subchannelAddressString +
337 ' connection closed with error ' +
338 error.message);
339 });
340 }
341 startConnectingInternal() {
342 var _a, _b;
343 /* Pass connection options through to the proxy so that it's able to
344 * upgrade it's connection to support tls if needed.
345 * This is a workaround for https://github.com/nodejs/node/issues/32922
346 * See https://github.com/grpc/grpc-node/pull/1369 for more info. */
347 const connectionOptions = this.credentials._getConnectionOptions() || {};
348 if ('secureContext' in connectionOptions) {
349 connectionOptions.ALPNProtocols = ['h2'];
350 // If provided, the value of grpc.ssl_target_name_override should be used
351 // to override the target hostname when checking server identity.
352 // This option is used for testing only.
353 if (this.options['grpc.ssl_target_name_override']) {
354 const sslTargetNameOverride = this.options['grpc.ssl_target_name_override'];
355 connectionOptions.checkServerIdentity = (host, cert) => {
356 return tls_1.checkServerIdentity(sslTargetNameOverride, cert);
357 };
358 connectionOptions.servername = sslTargetNameOverride;
359 }
360 else {
361 if ('grpc.http_connect_target' in this.options) {
362 /* This is more or less how servername will be set in createSession
363 * if a connection is successfully established through the proxy.
364 * If the proxy is not used, these connectionOptions are discarded
365 * anyway */
366 const targetPath = resolver_1.getDefaultAuthority((_a = uri_parser_1.parseUri(this.options['grpc.http_connect_target'])) !== null && _a !== void 0 ? _a : {
367 path: 'localhost',
368 });
369 const hostPort = uri_parser_1.splitHostPort(targetPath);
370 connectionOptions.servername = (_b = hostPort === null || hostPort === void 0 ? void 0 : hostPort.host) !== null && _b !== void 0 ? _b : targetPath;
371 }
372 }
373 }
374 http_proxy_1.getProxiedConnection(this.subchannelAddress, this.options, connectionOptions).then((result) => {
375 this.createSession(result);
376 }, (reason) => {
377 this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.TRANSIENT_FAILURE);
378 });
379 }
380 /**
381 * Initiate a state transition from any element of oldStates to the new
382 * state. If the current connectivityState is not in oldStates, do nothing.
383 * @param oldStates The set of states to transition from
384 * @param newState The state to transition to
385 * @returns True if the state changed, false otherwise
386 */
387 transitionToState(oldStates, newState) {
388 if (oldStates.indexOf(this.connectivityState) === -1) {
389 return false;
390 }
391 trace(this.subchannelAddressString +
392 ' ' +
393 channel_1.ConnectivityState[this.connectivityState] +
394 ' -> ' +
395 channel_1.ConnectivityState[newState]);
396 const previousState = this.connectivityState;
397 this.connectivityState = newState;
398 switch (newState) {
399 case channel_1.ConnectivityState.READY:
400 this.stopBackoff();
401 this.session.socket.once('close', () => {
402 for (const listener of this.disconnectListeners) {
403 listener();
404 }
405 });
406 if (this.keepaliveWithoutCalls) {
407 this.startKeepalivePings();
408 }
409 break;
410 case channel_1.ConnectivityState.CONNECTING:
411 this.startBackoff();
412 this.startConnectingInternal();
413 this.continueConnecting = false;
414 break;
415 case channel_1.ConnectivityState.TRANSIENT_FAILURE:
416 if (this.session) {
417 this.session.close();
418 }
419 this.session = null;
420 this.stopKeepalivePings();
421 /* If the backoff timer has already ended by the time we get to the
422 * TRANSIENT_FAILURE state, we want to immediately transition out of
423 * TRANSIENT_FAILURE as though the backoff timer is ending right now */
424 if (!this.backoffTimeout.isRunning()) {
425 process.nextTick(() => {
426 this.handleBackoffTimer();
427 });
428 }
429 break;
430 case channel_1.ConnectivityState.IDLE:
431 if (this.session) {
432 this.session.close();
433 }
434 this.session = null;
435 this.stopKeepalivePings();
436 break;
437 default:
438 throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
439 }
440 /* We use a shallow copy of the stateListeners array in case a listener
441 * is removed during this iteration */
442 for (const listener of [...this.stateListeners]) {
443 listener(this, previousState, newState);
444 }
445 return true;
446 }
447 /**
448 * Check if the subchannel associated with zero calls and with zero channels.
449 * If so, shut it down.
450 */
451 checkBothRefcounts() {
452 /* If no calls, channels, or subchannel pools have any more references to
453 * this subchannel, we can be sure it will never be used again. */
454 if (this.callRefcount === 0 && this.refcount === 0) {
455 this.transitionToState([
456 channel_1.ConnectivityState.CONNECTING,
457 channel_1.ConnectivityState.READY,
458 ], channel_1.ConnectivityState.TRANSIENT_FAILURE);
459 }
460 }
461 callRef() {
462 refTrace(this.subchannelAddressString +
463 ' callRefcount ' +
464 this.callRefcount +
465 ' -> ' +
466 (this.callRefcount + 1));
467 if (this.callRefcount === 0) {
468 if (this.session) {
469 this.session.ref();
470 }
471 this.backoffTimeout.ref();
472 if (!this.keepaliveWithoutCalls) {
473 this.startKeepalivePings();
474 }
475 }
476 this.callRefcount += 1;
477 }
478 callUnref() {
479 refTrace(this.subchannelAddressString +
480 ' callRefcount ' +
481 this.callRefcount +
482 ' -> ' +
483 (this.callRefcount - 1));
484 this.callRefcount -= 1;
485 if (this.callRefcount === 0) {
486 if (this.session) {
487 this.session.unref();
488 }
489 this.backoffTimeout.unref();
490 if (!this.keepaliveWithoutCalls) {
491 this.stopKeepalivePings();
492 }
493 this.checkBothRefcounts();
494 }
495 }
496 ref() {
497 refTrace(this.subchannelAddressString +
498 ' refcount ' +
499 this.refcount +
500 ' -> ' +
501 (this.refcount + 1));
502 this.refcount += 1;
503 }
504 unref() {
505 refTrace(this.subchannelAddressString +
506 ' refcount ' +
507 this.refcount +
508 ' -> ' +
509 (this.refcount - 1));
510 this.refcount -= 1;
511 this.checkBothRefcounts();
512 }
513 unrefIfOneRef() {
514 if (this.refcount === 1) {
515 this.unref();
516 return true;
517 }
518 return false;
519 }
520 /**
521 * Start a stream on the current session with the given `metadata` as headers
522 * and then attach it to the `callStream`. Must only be called if the
523 * subchannel's current connectivity state is READY.
524 * @param metadata
525 * @param callStream
526 */
527 startCallStream(metadata, callStream, extraFilterFactory) {
528 const headers = metadata.toHttp2Headers();
529 headers[HTTP2_HEADER_AUTHORITY] = callStream.getHost();
530 headers[HTTP2_HEADER_USER_AGENT] = this.userAgent;
531 headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc';
532 headers[HTTP2_HEADER_METHOD] = 'POST';
533 headers[HTTP2_HEADER_PATH] = callStream.getMethod();
534 headers[HTTP2_HEADER_TE] = 'trailers';
535 let http2Stream;
536 /* In theory, if an error is thrown by session.request because session has
537 * become unusable (e.g. because it has received a goaway), this subchannel
538 * should soon see the corresponding close or goaway event anyway and leave
539 * READY. But we have seen reports that this does not happen
540 * (https://github.com/googleapis/nodejs-firestore/issues/1023#issuecomment-653204096)
541 * so for defense in depth, we just discard the session when we see an
542 * error here.
543 */
544 try {
545 http2Stream = this.session.request(headers);
546 }
547 catch (e) {
548 this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.TRANSIENT_FAILURE);
549 throw e;
550 }
551 let headersString = '';
552 for (const header of Object.keys(headers)) {
553 headersString += '\t\t' + header + ': ' + headers[header] + '\n';
554 }
555 logging.trace(constants_1.LogVerbosity.DEBUG, 'call_stream', 'Starting stream on subchannel ' + this.subchannelAddressString + ' with headers\n' + headersString);
556 callStream.attachHttp2Stream(http2Stream, this, extraFilterFactory);
557 }
558 /**
559 * If the subchannel is currently IDLE, start connecting and switch to the
560 * CONNECTING state. If the subchannel is current in TRANSIENT_FAILURE,
561 * the next time it would transition to IDLE, start connecting again instead.
562 * Otherwise, do nothing.
563 */
564 startConnecting() {
565 /* First, try to transition from IDLE to connecting. If that doesn't happen
566 * because the state is not currently IDLE, check if it is
567 * TRANSIENT_FAILURE, and if so indicate that it should go back to
568 * connecting after the backoff timer ends. Otherwise do nothing */
569 if (!this.transitionToState([channel_1.ConnectivityState.IDLE], channel_1.ConnectivityState.CONNECTING)) {
570 if (this.connectivityState === channel_1.ConnectivityState.TRANSIENT_FAILURE) {
571 this.continueConnecting = true;
572 }
573 }
574 }
575 /**
576 * Get the subchannel's current connectivity state.
577 */
578 getConnectivityState() {
579 return this.connectivityState;
580 }
581 /**
582 * Add a listener function to be called whenever the subchannel's
583 * connectivity state changes.
584 * @param listener
585 */
586 addConnectivityStateListener(listener) {
587 this.stateListeners.push(listener);
588 }
589 /**
590 * Remove a listener previously added with `addConnectivityStateListener`
591 * @param listener A reference to a function previously passed to
592 * `addConnectivityStateListener`
593 */
594 removeConnectivityStateListener(listener) {
595 const listenerIndex = this.stateListeners.indexOf(listener);
596 if (listenerIndex > -1) {
597 this.stateListeners.splice(listenerIndex, 1);
598 }
599 }
600 addDisconnectListener(listener) {
601 this.disconnectListeners.push(listener);
602 }
603 removeDisconnectListener(listener) {
604 const listenerIndex = this.disconnectListeners.indexOf(listener);
605 if (listenerIndex > -1) {
606 this.disconnectListeners.splice(listenerIndex, 1);
607 }
608 }
609 /**
610 * Reset the backoff timeout, and immediately start connecting if in backoff.
611 */
612 resetBackoff() {
613 this.backoffTimeout.reset();
614 this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.CONNECTING);
615 }
616 getAddress() {
617 return this.subchannelAddressString;
618 }
619}
620exports.Subchannel = Subchannel;
621//# sourceMappingURL=subchannel.js.map
\No newline at end of file