You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							397 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							397 lines
						
					
					
						
							15 KiB
						
					
					
				
								'use strict';
							 | 
						|
								
							 | 
						|
								const OriginalAgent = require('http').Agent;
							 | 
						|
								const ms = require('humanize-ms');
							 | 
						|
								const debug = require('debug')('agentkeepalive');
							 | 
						|
								const deprecate = require('depd')('agentkeepalive');
							 | 
						|
								const {
							 | 
						|
								  INIT_SOCKET,
							 | 
						|
								  CURRENT_ID,
							 | 
						|
								  CREATE_ID,
							 | 
						|
								  SOCKET_CREATED_TIME,
							 | 
						|
								  SOCKET_NAME,
							 | 
						|
								  SOCKET_REQUEST_COUNT,
							 | 
						|
								  SOCKET_REQUEST_FINISHED_COUNT,
							 | 
						|
								} = require('./constants');
							 | 
						|
								
							 | 
						|
								// OriginalAgent come from
							 | 
						|
								// - https://github.com/nodejs/node/blob/v8.12.0/lib/_http_agent.js
							 | 
						|
								// - https://github.com/nodejs/node/blob/v10.12.0/lib/_http_agent.js
							 | 
						|
								
							 | 
						|
								// node <= 10
							 | 
						|
								let defaultTimeoutListenerCount = 1;
							 | 
						|
								const majorVersion = parseInt(process.version.split('.', 1)[0].substring(1));
							 | 
						|
								if (majorVersion >= 11 && majorVersion <= 12) {
							 | 
						|
								  defaultTimeoutListenerCount = 2;
							 | 
						|
								} else if (majorVersion >= 13) {
							 | 
						|
								  defaultTimeoutListenerCount = 3;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								class Agent extends OriginalAgent {
							 | 
						|
								  constructor(options) {
							 | 
						|
								    options = options || {};
							 | 
						|
								    options.keepAlive = options.keepAlive !== false;
							 | 
						|
								    // default is keep-alive and 15s free socket timeout
							 | 
						|
								    if (options.freeSocketTimeout === undefined) {
							 | 
						|
								      options.freeSocketTimeout = 15000;
							 | 
						|
								    }
							 | 
						|
								    // Legacy API: keepAliveTimeout should be rename to `freeSocketTimeout`
							 | 
						|
								    if (options.keepAliveTimeout) {
							 | 
						|
								      deprecate('options.keepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
							 | 
						|
								      options.freeSocketTimeout = options.keepAliveTimeout;
							 | 
						|
								      delete options.keepAliveTimeout;
							 | 
						|
								    }
							 | 
						|
								    // Legacy API: freeSocketKeepAliveTimeout should be rename to `freeSocketTimeout`
							 | 
						|
								    if (options.freeSocketKeepAliveTimeout) {
							 | 
						|
								      deprecate('options.freeSocketKeepAliveTimeout is deprecated, please use options.freeSocketTimeout instead');
							 | 
						|
								      options.freeSocketTimeout = options.freeSocketKeepAliveTimeout;
							 | 
						|
								      delete options.freeSocketKeepAliveTimeout;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Sets the socket to timeout after timeout milliseconds of inactivity on the socket.
							 | 
						|
								    // By default is double free socket timeout.
							 | 
						|
								    if (options.timeout === undefined) {
							 | 
						|
								      // make sure socket default inactivity timeout >= 30s
							 | 
						|
								      options.timeout = Math.max(options.freeSocketTimeout * 2, 30000);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // support humanize format
							 | 
						|
								    options.timeout = ms(options.timeout);
							 | 
						|
								    options.freeSocketTimeout = ms(options.freeSocketTimeout);
							 | 
						|
								    options.socketActiveTTL = options.socketActiveTTL ? ms(options.socketActiveTTL) : 0;
							 | 
						|
								
							 | 
						|
								    super(options);
							 | 
						|
								
							 | 
						|
								    this[CURRENT_ID] = 0;
							 | 
						|
								
							 | 
						|
								    // create socket success counter
							 | 
						|
								    this.createSocketCount = 0;
							 | 
						|
								    this.createSocketCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    this.createSocketErrorCount = 0;
							 | 
						|
								    this.createSocketErrorCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    this.closeSocketCount = 0;
							 | 
						|
								    this.closeSocketCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    // socket error event count
							 | 
						|
								    this.errorSocketCount = 0;
							 | 
						|
								    this.errorSocketCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    // request finished counter
							 | 
						|
								    this.requestCount = 0;
							 | 
						|
								    this.requestCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    // including free socket timeout counter
							 | 
						|
								    this.timeoutSocketCount = 0;
							 | 
						|
								    this.timeoutSocketCountLastCheck = 0;
							 | 
						|
								
							 | 
						|
								    this.on('free', socket => {
							 | 
						|
								      // https://github.com/nodejs/node/pull/32000
							 | 
						|
								      // Node.js native agent will check socket timeout eqs agent.options.timeout.
							 | 
						|
								      // Use the ttl or freeSocketTimeout to overwrite.
							 | 
						|
								      const timeout = this.calcSocketTimeout(socket);
							 | 
						|
								      if (timeout > 0 && socket.timeout !== timeout) {
							 | 
						|
								        socket.setTimeout(timeout);
							 | 
						|
								      }
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get freeSocketKeepAliveTimeout() {
							 | 
						|
								    deprecate('agent.freeSocketKeepAliveTimeout is deprecated, please use agent.options.freeSocketTimeout instead');
							 | 
						|
								    return this.options.freeSocketTimeout;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get timeout() {
							 | 
						|
								    deprecate('agent.timeout is deprecated, please use agent.options.timeout instead');
							 | 
						|
								    return this.options.timeout;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get socketActiveTTL() {
							 | 
						|
								    deprecate('agent.socketActiveTTL is deprecated, please use agent.options.socketActiveTTL instead');
							 | 
						|
								    return this.options.socketActiveTTL;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  calcSocketTimeout(socket) {
							 | 
						|
								    /**
							 | 
						|
								     * return <= 0: should free socket
							 | 
						|
								     * return > 0: should update socket timeout
							 | 
						|
								     * return undefined: not find custom timeout
							 | 
						|
								     */
							 | 
						|
								    let freeSocketTimeout = this.options.freeSocketTimeout;
							 | 
						|
								    const socketActiveTTL = this.options.socketActiveTTL;
							 | 
						|
								    if (socketActiveTTL) {
							 | 
						|
								      // check socketActiveTTL
							 | 
						|
								      const aliveTime = Date.now() - socket[SOCKET_CREATED_TIME];
							 | 
						|
								      const diff = socketActiveTTL - aliveTime;
							 | 
						|
								      if (diff <= 0) {
							 | 
						|
								        return diff;
							 | 
						|
								      }
							 | 
						|
								      if (freeSocketTimeout && diff < freeSocketTimeout) {
							 | 
						|
								        freeSocketTimeout = diff;
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								    // set freeSocketTimeout
							 | 
						|
								    if (freeSocketTimeout) {
							 | 
						|
								      // set free keepalive timer
							 | 
						|
								      // try to use socket custom freeSocketTimeout first, support headers['keep-alive']
							 | 
						|
								      // https://github.com/node-modules/urllib/blob/b76053020923f4d99a1c93cf2e16e0c5ba10bacf/lib/urllib.js#L498
							 | 
						|
								      const customFreeSocketTimeout = socket.freeSocketTimeout || socket.freeSocketKeepAliveTimeout;
							 | 
						|
								      return customFreeSocketTimeout || freeSocketTimeout;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  keepSocketAlive(socket) {
							 | 
						|
								    const result = super.keepSocketAlive(socket);
							 | 
						|
								    // should not keepAlive, do nothing
							 | 
						|
								    if (!result) return result;
							 | 
						|
								
							 | 
						|
								    const customTimeout = this.calcSocketTimeout(socket);
							 | 
						|
								    if (typeof customTimeout === 'undefined') {
							 | 
						|
								      return true;
							 | 
						|
								    }
							 | 
						|
								    if (customTimeout <= 0) {
							 | 
						|
								      debug('%s(requests: %s, finished: %s) free but need to destroy by TTL, request count %s, diff is %s',
							 | 
						|
								        socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], customTimeout);
							 | 
						|
								      return false;
							 | 
						|
								    }
							 | 
						|
								    if (socket.timeout !== customTimeout) {
							 | 
						|
								      socket.setTimeout(customTimeout);
							 | 
						|
								    }
							 | 
						|
								    return true;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // only call on addRequest
							 | 
						|
								  reuseSocket(...args) {
							 | 
						|
								    // reuseSocket(socket, req)
							 | 
						|
								    super.reuseSocket(...args);
							 | 
						|
								    const socket = args[0];
							 | 
						|
								    const req = args[1];
							 | 
						|
								    req.reusedSocket = true;
							 | 
						|
								    const agentTimeout = this.options.timeout;
							 | 
						|
								    if (getSocketTimeout(socket) !== agentTimeout) {
							 | 
						|
								      // reset timeout before use
							 | 
						|
								      socket.setTimeout(agentTimeout);
							 | 
						|
								      debug('%s reset timeout to %sms', socket[SOCKET_NAME], agentTimeout);
							 | 
						|
								    }
							 | 
						|
								    socket[SOCKET_REQUEST_COUNT]++;
							 | 
						|
								    debug('%s(requests: %s, finished: %s) reuse on addRequest, timeout %sms',
							 | 
						|
								      socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
							 | 
						|
								      getSocketTimeout(socket));
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  [CREATE_ID]() {
							 | 
						|
								    const id = this[CURRENT_ID]++;
							 | 
						|
								    if (this[CURRENT_ID] === Number.MAX_SAFE_INTEGER) this[CURRENT_ID] = 0;
							 | 
						|
								    return id;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  [INIT_SOCKET](socket, options) {
							 | 
						|
								    // bugfix here.
							 | 
						|
								    // https on node 8, 10 won't set agent.options.timeout by default
							 | 
						|
								    // TODO: need to fix on node itself
							 | 
						|
								    if (options.timeout) {
							 | 
						|
								      const timeout = getSocketTimeout(socket);
							 | 
						|
								      if (!timeout) {
							 | 
						|
								        socket.setTimeout(options.timeout);
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    if (this.options.keepAlive) {
							 | 
						|
								      // Disable Nagle's algorithm: http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
							 | 
						|
								      // https://fengmk2.com/benchmark/nagle-algorithm-delayed-ack-mock.html
							 | 
						|
								      socket.setNoDelay(true);
							 | 
						|
								    }
							 | 
						|
								    this.createSocketCount++;
							 | 
						|
								    if (this.options.socketActiveTTL) {
							 | 
						|
								      socket[SOCKET_CREATED_TIME] = Date.now();
							 | 
						|
								    }
							 | 
						|
								    // don't show the hole '-----BEGIN CERTIFICATE----' key string
							 | 
						|
								    socket[SOCKET_NAME] = `sock[${this[CREATE_ID]()}#${options._agentKey}]`.split('-----BEGIN', 1)[0];
							 | 
						|
								    socket[SOCKET_REQUEST_COUNT] = 1;
							 | 
						|
								    socket[SOCKET_REQUEST_FINISHED_COUNT] = 0;
							 | 
						|
								    installListeners(this, socket, options);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  createConnection(options, oncreate) {
							 | 
						|
								    let called = false;
							 | 
						|
								    const onNewCreate = (err, socket) => {
							 | 
						|
								      if (called) return;
							 | 
						|
								      called = true;
							 | 
						|
								
							 | 
						|
								      if (err) {
							 | 
						|
								        this.createSocketErrorCount++;
							 | 
						|
								        return oncreate(err);
							 | 
						|
								      }
							 | 
						|
								      this[INIT_SOCKET](socket, options);
							 | 
						|
								      oncreate(err, socket);
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    const newSocket = super.createConnection(options, onNewCreate);
							 | 
						|
								    if (newSocket) onNewCreate(null, newSocket);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get statusChanged() {
							 | 
						|
								    const changed = this.createSocketCount !== this.createSocketCountLastCheck ||
							 | 
						|
								      this.createSocketErrorCount !== this.createSocketErrorCountLastCheck ||
							 | 
						|
								      this.closeSocketCount !== this.closeSocketCountLastCheck ||
							 | 
						|
								      this.errorSocketCount !== this.errorSocketCountLastCheck ||
							 | 
						|
								      this.timeoutSocketCount !== this.timeoutSocketCountLastCheck ||
							 | 
						|
								      this.requestCount !== this.requestCountLastCheck;
							 | 
						|
								    if (changed) {
							 | 
						|
								      this.createSocketCountLastCheck = this.createSocketCount;
							 | 
						|
								      this.createSocketErrorCountLastCheck = this.createSocketErrorCount;
							 | 
						|
								      this.closeSocketCountLastCheck = this.closeSocketCount;
							 | 
						|
								      this.errorSocketCountLastCheck = this.errorSocketCount;
							 | 
						|
								      this.timeoutSocketCountLastCheck = this.timeoutSocketCount;
							 | 
						|
								      this.requestCountLastCheck = this.requestCount;
							 | 
						|
								    }
							 | 
						|
								    return changed;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  getCurrentStatus() {
							 | 
						|
								    return {
							 | 
						|
								      createSocketCount: this.createSocketCount,
							 | 
						|
								      createSocketErrorCount: this.createSocketErrorCount,
							 | 
						|
								      closeSocketCount: this.closeSocketCount,
							 | 
						|
								      errorSocketCount: this.errorSocketCount,
							 | 
						|
								      timeoutSocketCount: this.timeoutSocketCount,
							 | 
						|
								      requestCount: this.requestCount,
							 | 
						|
								      freeSockets: inspect(this.freeSockets),
							 | 
						|
								      sockets: inspect(this.sockets),
							 | 
						|
								      requests: inspect(this.requests),
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// node 8 don't has timeout attribute on socket
							 | 
						|
								// https://github.com/nodejs/node/pull/21204/files#diff-e6ef024c3775d787c38487a6309e491dR408
							 | 
						|
								function getSocketTimeout(socket) {
							 | 
						|
								  return socket.timeout || socket._idleTimeout;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function installListeners(agent, socket, options) {
							 | 
						|
								  debug('%s create, timeout %sms', socket[SOCKET_NAME], getSocketTimeout(socket));
							 | 
						|
								
							 | 
						|
								  // listener socket events: close, timeout, error, free
							 | 
						|
								  function onFree() {
							 | 
						|
								    // create and socket.emit('free') logic
							 | 
						|
								    // https://github.com/nodejs/node/blob/master/lib/_http_agent.js#L311
							 | 
						|
								    // no req on the socket, it should be the new socket
							 | 
						|
								    if (!socket._httpMessage && socket[SOCKET_REQUEST_COUNT] === 1) return;
							 | 
						|
								
							 | 
						|
								    socket[SOCKET_REQUEST_FINISHED_COUNT]++;
							 | 
						|
								    agent.requestCount++;
							 | 
						|
								    debug('%s(requests: %s, finished: %s) free',
							 | 
						|
								      socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
							 | 
						|
								
							 | 
						|
								    // should reuse on pedding requests?
							 | 
						|
								    const name = agent.getName(options);
							 | 
						|
								    if (socket.writable && agent.requests[name] && agent.requests[name].length) {
							 | 
						|
								      // will be reuse on agent free listener
							 | 
						|
								      socket[SOCKET_REQUEST_COUNT]++;
							 | 
						|
								      debug('%s(requests: %s, finished: %s) will be reuse on agent free event',
							 | 
						|
								        socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  socket.on('free', onFree);
							 | 
						|
								
							 | 
						|
								  function onClose(isError) {
							 | 
						|
								    debug('%s(requests: %s, finished: %s) close, isError: %s',
							 | 
						|
								      socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], isError);
							 | 
						|
								    agent.closeSocketCount++;
							 | 
						|
								  }
							 | 
						|
								  socket.on('close', onClose);
							 | 
						|
								
							 | 
						|
								  // start socket timeout handler
							 | 
						|
								  function onTimeout() {
							 | 
						|
								    // onTimeout and emitRequestTimeout(_http_client.js)
							 | 
						|
								    // https://github.com/nodejs/node/blob/v12.x/lib/_http_client.js#L711
							 | 
						|
								    const listenerCount = socket.listeners('timeout').length;
							 | 
						|
								    // node <= 10, default listenerCount is 1, onTimeout
							 | 
						|
								    // 11 < node <= 12, default listenerCount is 2, onTimeout and emitRequestTimeout
							 | 
						|
								    // node >= 13, default listenerCount is 3, onTimeout,
							 | 
						|
								    //   onTimeout(https://github.com/nodejs/node/pull/32000/files#diff-5f7fb0850412c6be189faeddea6c5359R333)
							 | 
						|
								    //   and emitRequestTimeout
							 | 
						|
								    const timeout = getSocketTimeout(socket);
							 | 
						|
								    const req = socket._httpMessage;
							 | 
						|
								    const reqTimeoutListenerCount = req && req.listeners('timeout').length || 0;
							 | 
						|
								    debug('%s(requests: %s, finished: %s) timeout after %sms, listeners %s, defaultTimeoutListenerCount %s, hasHttpRequest %s, HttpRequest timeoutListenerCount %s',
							 | 
						|
								      socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
							 | 
						|
								      timeout, listenerCount, defaultTimeoutListenerCount, !!req, reqTimeoutListenerCount);
							 | 
						|
								    if (debug.enabled) {
							 | 
						|
								      debug('timeout listeners: %s', socket.listeners('timeout').map(f => f.name).join(', '));
							 | 
						|
								    }
							 | 
						|
								    agent.timeoutSocketCount++;
							 | 
						|
								    const name = agent.getName(options);
							 | 
						|
								    if (agent.freeSockets[name] && agent.freeSockets[name].indexOf(socket) !== -1) {
							 | 
						|
								      // free socket timeout, destroy quietly
							 | 
						|
								      socket.destroy();
							 | 
						|
								      // Remove it from freeSockets list immediately to prevent new requests
							 | 
						|
								      // from being sent through this socket.
							 | 
						|
								      agent.removeSocket(socket, options);
							 | 
						|
								      debug('%s is free, destroy quietly', socket[SOCKET_NAME]);
							 | 
						|
								    } else {
							 | 
						|
								      // if there is no any request socket timeout handler,
							 | 
						|
								      // agent need to handle socket timeout itself.
							 | 
						|
								      //
							 | 
						|
								      // custom request socket timeout handle logic must follow these rules:
							 | 
						|
								      //  1. Destroy socket first
							 | 
						|
								      //  2. Must emit socket 'agentRemove' event tell agent remove socket
							 | 
						|
								      //     from freeSockets list immediately.
							 | 
						|
								      //     Otherise you may be get 'socket hang up' error when reuse
							 | 
						|
								      //     free socket and timeout happen in the same time.
							 | 
						|
								      if (reqTimeoutListenerCount === 0) {
							 | 
						|
								        const error = new Error('Socket timeout');
							 | 
						|
								        error.code = 'ERR_SOCKET_TIMEOUT';
							 | 
						|
								        error.timeout = timeout;
							 | 
						|
								        // must manually call socket.end() or socket.destroy() to end the connection.
							 | 
						|
								        // https://nodejs.org/dist/latest-v10.x/docs/api/net.html#net_socket_settimeout_timeout_callback
							 | 
						|
								        socket.destroy(error);
							 | 
						|
								        agent.removeSocket(socket, options);
							 | 
						|
								        debug('%s destroy with timeout error', socket[SOCKET_NAME]);
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  socket.on('timeout', onTimeout);
							 | 
						|
								
							 | 
						|
								  function onError(err) {
							 | 
						|
								    const listenerCount = socket.listeners('error').length;
							 | 
						|
								    debug('%s(requests: %s, finished: %s) error: %s, listenerCount: %s',
							 | 
						|
								      socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
							 | 
						|
								      err, listenerCount);
							 | 
						|
								    agent.errorSocketCount++;
							 | 
						|
								    if (listenerCount === 1) {
							 | 
						|
								      // if socket don't contain error event handler, don't catch it, emit it again
							 | 
						|
								      debug('%s emit uncaught error event', socket[SOCKET_NAME]);
							 | 
						|
								      socket.removeListener('error', onError);
							 | 
						|
								      socket.emit('error', err);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  socket.on('error', onError);
							 | 
						|
								
							 | 
						|
								  function onRemove() {
							 | 
						|
								    debug('%s(requests: %s, finished: %s) agentRemove',
							 | 
						|
								      socket[SOCKET_NAME],
							 | 
						|
								      socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT]);
							 | 
						|
								    // We need this function for cases like HTTP 'upgrade'
							 | 
						|
								    // (defined by WebSockets) where we need to remove a socket from the
							 | 
						|
								    // pool because it'll be locked up indefinitely
							 | 
						|
								    socket.removeListener('close', onClose);
							 | 
						|
								    socket.removeListener('error', onError);
							 | 
						|
								    socket.removeListener('free', onFree);
							 | 
						|
								    socket.removeListener('timeout', onTimeout);
							 | 
						|
								    socket.removeListener('agentRemove', onRemove);
							 | 
						|
								  }
							 | 
						|
								  socket.on('agentRemove', onRemove);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								module.exports = Agent;
							 | 
						|
								
							 | 
						|
								function inspect(obj) {
							 | 
						|
								  const res = {};
							 | 
						|
								  for (const key in obj) {
							 | 
						|
								    res[key] = obj[key].length;
							 | 
						|
								  }
							 | 
						|
								  return res;
							 | 
						|
								}
							 |