Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						9866c2bfff
					
				
					 21 changed files with 701 additions and 152 deletions
				
			
		
							
								
								
									
										5
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Gemfile
									
									
									
									
									
								
							|  | @ -111,7 +111,7 @@ group :production, :test do | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| group :test do | group :test do | ||||||
|   gem 'capybara', '~> 3.24' |   gem 'capybara', '~> 3.25' | ||||||
|   gem 'climate_control', '~> 0.2' |   gem 'climate_control', '~> 0.2' | ||||||
|   gem 'faker', '~> 1.9' |   gem 'faker', '~> 1.9' | ||||||
|   gem 'microformats', '~> 4.1' |   gem 'microformats', '~> 4.1' | ||||||
|  | @ -131,7 +131,7 @@ group :development do | ||||||
|   gem 'letter_opener', '~> 1.7' |   gem 'letter_opener', '~> 1.7' | ||||||
|   gem 'letter_opener_web', '~> 1.3' |   gem 'letter_opener_web', '~> 1.3' | ||||||
|   gem 'memory_profiler' |   gem 'memory_profiler' | ||||||
|   gem 'rubocop', '~> 0.71', require: false |   gem 'rubocop', '~> 0.72', require: false | ||||||
|   gem 'rubocop-rails', '~> 2.0', require: false |   gem 'rubocop-rails', '~> 2.0', require: false | ||||||
|   gem 'brakeman', '~> 4.5', require: false |   gem 'brakeman', '~> 4.5', require: false | ||||||
|   gem 'bundler-audit', '~> 0.6', require: false |   gem 'bundler-audit', '~> 0.6', require: false | ||||||
|  | @ -151,3 +151,4 @@ group :production do | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| gem 'concurrent-ruby', require: false | gem 'concurrent-ruby', require: false | ||||||
|  | gem 'connection_pool', require: false | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								Gemfile.lock
									
									
									
									
									
								
							|  | @ -106,7 +106,7 @@ GEM | ||||||
|     brakeman (4.5.1) |     brakeman (4.5.1) | ||||||
|     browser (2.5.3) |     browser (2.5.3) | ||||||
|     builder (3.2.3) |     builder (3.2.3) | ||||||
|     bullet (6.0.0) |     bullet (6.0.1) | ||||||
|       activesupport (>= 3.0.0) |       activesupport (>= 3.0.0) | ||||||
|       uniform_notifier (~> 1.11) |       uniform_notifier (~> 1.11) | ||||||
|     bundler-audit (0.6.1) |     bundler-audit (0.6.1) | ||||||
|  | @ -129,7 +129,7 @@ GEM | ||||||
|       sshkit (~> 1.3) |       sshkit (~> 1.3) | ||||||
|     capistrano-yarn (2.0.2) |     capistrano-yarn (2.0.2) | ||||||
|       capistrano (~> 3.0) |       capistrano (~> 3.0) | ||||||
|     capybara (3.24.0) |     capybara (3.25.0) | ||||||
|       addressable |       addressable | ||||||
|       mini_mime (>= 0.1.3) |       mini_mime (>= 0.1.3) | ||||||
|       nokogiri (~> 1.8) |       nokogiri (~> 1.8) | ||||||
|  | @ -289,7 +289,7 @@ GEM | ||||||
|     idn-ruby (0.1.0) |     idn-ruby (0.1.0) | ||||||
|     ipaddress (0.8.3) |     ipaddress (0.8.3) | ||||||
|     iso-639 (0.2.8) |     iso-639 (0.2.8) | ||||||
|     jaro_winkler (1.5.2) |     jaro_winkler (1.5.3) | ||||||
|     jmespath (1.4.0) |     jmespath (1.4.0) | ||||||
|     json (2.1.0) |     json (2.1.0) | ||||||
|     json-ld (3.0.2) |     json-ld (3.0.2) | ||||||
|  | @ -338,7 +338,7 @@ GEM | ||||||
|       mimemagic (~> 0.3.2) |       mimemagic (~> 0.3.2) | ||||||
|     mario-redis-lock (1.2.1) |     mario-redis-lock (1.2.1) | ||||||
|       redis (>= 3.0.5) |       redis (>= 3.0.5) | ||||||
|     memory_profiler (0.9.13) |     memory_profiler (0.9.14) | ||||||
|     method_source (0.9.2) |     method_source (0.9.2) | ||||||
|     microformats (4.1.0) |     microformats (4.1.0) | ||||||
|       json (~> 2.1) |       json (~> 2.1) | ||||||
|  | @ -422,7 +422,7 @@ GEM | ||||||
|       pry (~> 0.10) |       pry (~> 0.10) | ||||||
|     pry-rails (0.3.9) |     pry-rails (0.3.9) | ||||||
|       pry (>= 0.10.4) |       pry (>= 0.10.4) | ||||||
|     public_suffix (3.1.0) |     public_suffix (3.1.1) | ||||||
|     puma (3.12.1) |     puma (3.12.1) | ||||||
|     pundit (2.0.1) |     pundit (2.0.1) | ||||||
|       activesupport (>= 3.0.0) |       activesupport (>= 3.0.0) | ||||||
|  | @ -527,7 +527,7 @@ GEM | ||||||
|       rspec-core (~> 3.0, >= 3.0.0) |       rspec-core (~> 3.0, >= 3.0.0) | ||||||
|       sidekiq (>= 2.4.0) |       sidekiq (>= 2.4.0) | ||||||
|     rspec-support (3.8.0) |     rspec-support (3.8.0) | ||||||
|     rubocop (0.71.0) |     rubocop (0.72.0) | ||||||
|       jaro_winkler (~> 1.5.1) |       jaro_winkler (~> 1.5.1) | ||||||
|       parallel (~> 1.10) |       parallel (~> 1.10) | ||||||
|       parser (>= 2.6) |       parser (>= 2.6) | ||||||
|  | @ -663,12 +663,13 @@ DEPENDENCIES | ||||||
|   capistrano-rails (~> 1.4) |   capistrano-rails (~> 1.4) | ||||||
|   capistrano-rbenv (~> 2.1) |   capistrano-rbenv (~> 2.1) | ||||||
|   capistrano-yarn (~> 2.0) |   capistrano-yarn (~> 2.0) | ||||||
|   capybara (~> 3.24) |   capybara (~> 3.25) | ||||||
|   charlock_holmes (~> 0.7.6) |   charlock_holmes (~> 0.7.6) | ||||||
|   chewy (~> 5.0) |   chewy (~> 5.0) | ||||||
|   cld3 (~> 3.2.4) |   cld3 (~> 3.2.4) | ||||||
|   climate_control (~> 0.2) |   climate_control (~> 0.2) | ||||||
|   concurrent-ruby |   concurrent-ruby | ||||||
|  |   connection_pool | ||||||
|   derailed_benchmarks |   derailed_benchmarks | ||||||
|   devise (~> 4.6) |   devise (~> 4.6) | ||||||
|   devise-two-factor (~> 3.0) |   devise-two-factor (~> 3.0) | ||||||
|  | @ -742,7 +743,7 @@ DEPENDENCIES | ||||||
|   rqrcode (~> 0.10) |   rqrcode (~> 0.10) | ||||||
|   rspec-rails (~> 3.8) |   rspec-rails (~> 3.8) | ||||||
|   rspec-sidekiq (~> 3.0) |   rspec-sidekiq (~> 3.0) | ||||||
|   rubocop (~> 0.71) |   rubocop (~> 0.72) | ||||||
|   rubocop-rails (~> 2.0) |   rubocop-rails (~> 2.0) | ||||||
|   sanitize (~> 5.0) |   sanitize (~> 5.0) | ||||||
|   sidekiq (~> 5.2) |   sidekiq (~> 5.2) | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import { connect } from 'react-redux'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
|  | import { createSelector } from 'reselect'; | ||||||
| import { fetchStatus } from '../../actions/statuses'; | import { fetchStatus } from '../../actions/statuses'; | ||||||
| import MissingIndicator from '../../components/missing_indicator'; | import MissingIndicator from '../../components/missing_indicator'; | ||||||
| import DetailedStatus from './components/detailed_status'; | import DetailedStatus from './components/detailed_status'; | ||||||
|  | @ -63,39 +64,58 @@ const messages = defineMessages({ | ||||||
| const makeMapStateToProps = () => { | const makeMapStateToProps = () => { | ||||||
|   const getStatus = makeGetStatus(); |   const getStatus = makeGetStatus(); | ||||||
| 
 | 
 | ||||||
|  |   const getAncestorsIds = createSelector([ | ||||||
|  |     (_, { id }) => id, | ||||||
|  |     state => state.getIn(['contexts', 'inReplyTos']), | ||||||
|  |   ], (statusId, inReplyTos) => { | ||||||
|  |     let ancestorsIds = Immutable.List(); | ||||||
|  |     ancestorsIds = ancestorsIds.withMutations(mutable => { | ||||||
|  |       let id = statusId; | ||||||
|  | 
 | ||||||
|  |       while (id) { | ||||||
|  |         mutable.unshift(id); | ||||||
|  |         id = inReplyTos.get(id); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return ancestorsIds; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const getDescendantsIds = createSelector([ | ||||||
|  |     (_, { id }) => id, | ||||||
|  |     state => state.getIn(['contexts', 'replies']), | ||||||
|  |   ], (statusId, contextReplies) => { | ||||||
|  |     let descendantsIds = Immutable.List(); | ||||||
|  |     descendantsIds = descendantsIds.withMutations(mutable => { | ||||||
|  |       const ids = [statusId]; | ||||||
|  | 
 | ||||||
|  |       while (ids.length > 0) { | ||||||
|  |         let id        = ids.shift(); | ||||||
|  |         const replies = contextReplies.get(id); | ||||||
|  | 
 | ||||||
|  |         if (statusId !== id) { | ||||||
|  |           mutable.push(id); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (replies) { | ||||||
|  |           replies.reverse().forEach(reply => { | ||||||
|  |             ids.unshift(reply); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return descendantsIds; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   const mapStateToProps = (state, props) => { |   const mapStateToProps = (state, props) => { | ||||||
|     const status = getStatus(state, { id: props.params.statusId }); |     const status = getStatus(state, { id: props.params.statusId }); | ||||||
|     let ancestorsIds = Immutable.List(); |     let ancestorsIds = Immutable.List(); | ||||||
|     let descendantsIds = Immutable.List(); |     let descendantsIds = Immutable.List(); | ||||||
| 
 | 
 | ||||||
|     if (status) { |     if (status) { | ||||||
|       ancestorsIds = ancestorsIds.withMutations(mutable => { |       ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); | ||||||
|         let id = status.get('in_reply_to_id'); |       descendantsIds = getDescendantsIds(state, { id: status.get('id') }); | ||||||
| 
 |  | ||||||
|         while (id) { |  | ||||||
|           mutable.unshift(id); |  | ||||||
|           id = state.getIn(['contexts', 'inReplyTos', id]); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       descendantsIds = descendantsIds.withMutations(mutable => { |  | ||||||
|         const ids = [status.get('id')]; |  | ||||||
| 
 |  | ||||||
|         while (ids.length > 0) { |  | ||||||
|           let id        = ids.shift(); |  | ||||||
|           const replies = state.getIn(['contexts', 'replies', id]); |  | ||||||
| 
 |  | ||||||
|           if (status.get('id') !== id) { |  | ||||||
|             mutable.push(id); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           if (replies) { |  | ||||||
|             replies.reverse().forEach(reply => { |  | ||||||
|               ids.unshift(reply); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  |  | ||||||
|  | @ -195,6 +195,12 @@ const expandMentions = status => { | ||||||
|   return fragment.innerHTML; |   return fragment.innerHTML; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const expiresInFromExpiresAt = expires_at => { | ||||||
|  |   if (!expires_at) return 24 * 3600; | ||||||
|  |   const delta = (new Date(expires_at).getTime() - Date.now()) / 1000; | ||||||
|  |   return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export default function compose(state = initialState, action) { | export default function compose(state = initialState, action) { | ||||||
|   switch(action.type) { |   switch(action.type) { | ||||||
|   case STORE_HYDRATE: |   case STORE_HYDRATE: | ||||||
|  | @ -353,7 +359,7 @@ export default function compose(state = initialState, action) { | ||||||
|         map.set('poll', ImmutableMap({ |         map.set('poll', ImmutableMap({ | ||||||
|           options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), |           options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), | ||||||
|           multiple: action.status.getIn(['poll', 'multiple']), |           multiple: action.status.getIn(['poll', 'multiple']), | ||||||
|           expires_in: 24 * 3600, |           expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), | ||||||
|         })); |         })); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
							
								
								
									
										63
									
								
								app/lib/connection_pool/shared_connection_pool.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								app/lib/connection_pool/shared_connection_pool.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'connection_pool' | ||||||
|  | require_relative './shared_timed_stack' | ||||||
|  | 
 | ||||||
|  | class ConnectionPool::SharedConnectionPool < ConnectionPool | ||||||
|  |   def initialize(options = {}, &block) | ||||||
|  |     super(options, &block) | ||||||
|  | 
 | ||||||
|  |     @available = ConnectionPool::SharedTimedStack.new(@size, &block) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   delegate :size, :flush, to: :@available | ||||||
|  | 
 | ||||||
|  |   def with(preferred_tag, options = {}) | ||||||
|  |     Thread.handle_interrupt(Exception => :never) do | ||||||
|  |       conn = checkout(preferred_tag, options) | ||||||
|  | 
 | ||||||
|  |       begin | ||||||
|  |         Thread.handle_interrupt(Exception => :immediate) do | ||||||
|  |           yield conn | ||||||
|  |         end | ||||||
|  |       ensure | ||||||
|  |         checkin(preferred_tag) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def checkout(preferred_tag, options = {}) | ||||||
|  |     if ::Thread.current[key(preferred_tag)] | ||||||
|  |       ::Thread.current[key_count(preferred_tag)] += 1 | ||||||
|  |       ::Thread.current[key(preferred_tag)] | ||||||
|  |     else | ||||||
|  |       ::Thread.current[key_count(preferred_tag)] = 1 | ||||||
|  |       ::Thread.current[key(preferred_tag)] = @available.pop(preferred_tag, options[:timeout] || @timeout) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def checkin(preferred_tag) | ||||||
|  |     if ::Thread.current[key(preferred_tag)] | ||||||
|  |       if ::Thread.current[key_count(preferred_tag)] == 1 | ||||||
|  |         @available.push(::Thread.current[key(preferred_tag)]) | ||||||
|  |         ::Thread.current[key(preferred_tag)] = nil | ||||||
|  |       else | ||||||
|  |         ::Thread.current[key_count(preferred_tag)] -= 1 | ||||||
|  |       end | ||||||
|  |     else | ||||||
|  |       raise ConnectionPool::Error, 'no connections are checked out' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def key(tag) | ||||||
|  |     :"#{@key}-#{tag}" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def key_count(tag) | ||||||
|  |     :"#{@key_count}-#{tag}" | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										95
									
								
								app/lib/connection_pool/shared_timed_stack.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/lib/connection_pool/shared_timed_stack.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ConnectionPool::SharedTimedStack | ||||||
|  |   def initialize(max = 0, &block) | ||||||
|  |     @create_block = block | ||||||
|  |     @max          = max | ||||||
|  |     @created      = 0 | ||||||
|  |     @queue        = [] | ||||||
|  |     @tagged_queue = Hash.new { |hash, key| hash[key] = [] } | ||||||
|  |     @mutex        = Mutex.new | ||||||
|  |     @resource     = ConditionVariable.new | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def push(connection) | ||||||
|  |     @mutex.synchronize do | ||||||
|  |       store_connection(connection) | ||||||
|  |       @resource.broadcast | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   alias << push | ||||||
|  | 
 | ||||||
|  |   def pop(preferred_tag, timeout = 5.0) | ||||||
|  |     deadline = current_time + timeout | ||||||
|  | 
 | ||||||
|  |     @mutex.synchronize do | ||||||
|  |       loop do | ||||||
|  |         return fetch_preferred_connection(preferred_tag) unless @tagged_queue[preferred_tag].empty? | ||||||
|  | 
 | ||||||
|  |         connection = try_create(preferred_tag) | ||||||
|  |         return connection if connection | ||||||
|  | 
 | ||||||
|  |         to_wait = deadline - current_time | ||||||
|  |         raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0 | ||||||
|  | 
 | ||||||
|  |         @resource.wait(@mutex, to_wait) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def empty? | ||||||
|  |     size.zero? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def size | ||||||
|  |     @mutex.synchronize do | ||||||
|  |       @queue.size | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def flush | ||||||
|  |     @mutex.synchronize do | ||||||
|  |       @queue.delete_if do |connection| | ||||||
|  |         delete = !connection.in_use && (connection.dead || connection.seconds_idle >= RequestPool::MAX_IDLE_TIME) | ||||||
|  | 
 | ||||||
|  |         if delete | ||||||
|  |           @tagged_queue[connection.site].delete(connection) | ||||||
|  |           connection.close | ||||||
|  |           @created -= 1 | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         delete | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def try_create(preferred_tag) | ||||||
|  |     if @created == @max && !@queue.empty? | ||||||
|  |       throw_away_connection = @queue.pop | ||||||
|  |       @tagged_queue[throw_away_connection.site].delete(throw_away_connection) | ||||||
|  |       @create_block.call(preferred_tag) | ||||||
|  |     elsif @created != @max | ||||||
|  |       connection = @create_block.call(preferred_tag) | ||||||
|  |       @created += 1 | ||||||
|  |       connection | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def fetch_preferred_connection(preferred_tag) | ||||||
|  |     connection = @tagged_queue[preferred_tag].pop | ||||||
|  |     @queue.delete(connection) | ||||||
|  |     connection | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def current_time | ||||||
|  |     Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def store_connection(connection) | ||||||
|  |     @tagged_queue[connection.site].push(connection) | ||||||
|  |     @queue.push(connection) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -17,15 +17,21 @@ end | ||||||
| class Request | class Request | ||||||
|   REQUEST_TARGET = '(request-target)' |   REQUEST_TARGET = '(request-target)' | ||||||
| 
 | 
 | ||||||
|  |   # We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening | ||||||
|  |   # and 5s timeout on the TLS handshake, meaning the worst case should take | ||||||
|  |   # about 15s in total | ||||||
|  |   TIMEOUT = { connect: 5, read: 10, write: 10 }.freeze | ||||||
|  | 
 | ||||||
|   include RoutingHelper |   include RoutingHelper | ||||||
| 
 | 
 | ||||||
|   def initialize(verb, url, **options) |   def initialize(verb, url, **options) | ||||||
|     raise ArgumentError if url.blank? |     raise ArgumentError if url.blank? | ||||||
| 
 | 
 | ||||||
|     @verb    = verb |     @verb        = verb | ||||||
|     @url     = Addressable::URI.parse(url).normalize |     @url         = Addressable::URI.parse(url).normalize | ||||||
|     @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket }) |     @http_client = options.delete(:http_client) | ||||||
|     @headers = {} |     @options     = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket }) | ||||||
|  |     @headers     = {} | ||||||
| 
 | 
 | ||||||
|     raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service? |     raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service? | ||||||
| 
 | 
 | ||||||
|  | @ -50,15 +56,24 @@ class Request | ||||||
| 
 | 
 | ||||||
|   def perform |   def perform | ||||||
|     begin |     begin | ||||||
|       response = http_client.headers(headers).public_send(@verb, @url.to_s, @options) |       response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers)) | ||||||
|     rescue => e |     rescue => e | ||||||
|       raise e.class, "#{e.message} on #{@url}", e.backtrace[0] |       raise e.class, "#{e.message} on #{@url}", e.backtrace[0] | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     begin |     begin | ||||||
|       yield response.extend(ClientLimit) if block_given? |       response = response.extend(ClientLimit) | ||||||
|  | 
 | ||||||
|  |       # If we are using a persistent connection, we have to | ||||||
|  |       # read every response to be able to move forward at all. | ||||||
|  |       # However, simply calling #to_s or #flush may not be safe, | ||||||
|  |       # as the response body, if malicious, could be too big | ||||||
|  |       # for our memory. So we use the #body_with_limit method | ||||||
|  |       response.body_with_limit if http_client.persistent? | ||||||
|  | 
 | ||||||
|  |       yield response if block_given? | ||||||
|     ensure |     ensure | ||||||
|       http_client.close |       http_client.close unless http_client.persistent? | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -76,6 +91,10 @@ class Request | ||||||
| 
 | 
 | ||||||
|       %w(http https).include?(parsed_url.scheme) && parsed_url.host.present? |       %w(http https).include?(parsed_url.scheme) && parsed_url.host.present? | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     def http_client | ||||||
|  |       HTTP.use(:auto_inflate).timeout(:per_operation, TIMEOUT.dup).follow(max_hops: 2) | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  | @ -116,16 +135,8 @@ class Request | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def timeout |  | ||||||
|     # We enforce a 1s timeout on DNS resolving, 10s timeout on socket opening |  | ||||||
|     # and 5s timeout on the TLS handshake, meaning the worst case should take |  | ||||||
|     # about 16s in total |  | ||||||
| 
 |  | ||||||
|     { connect: 5, read: 10, write: 10 } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def http_client |   def http_client | ||||||
|     @http_client ||= HTTP.use(:auto_inflate).timeout(:per_operation, timeout).follow(max_hops: 2) |     @http_client ||= Request.http_client | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def use_proxy? |   def use_proxy? | ||||||
|  | @ -169,20 +180,41 @@ class Request | ||||||
|         return super(host, *args) if thru_hidden_service?(host) |         return super(host, *args) if thru_hidden_service?(host) | ||||||
| 
 | 
 | ||||||
|         outer_e = nil |         outer_e = nil | ||||||
|  |         port    = args.first | ||||||
| 
 | 
 | ||||||
|         Resolv::DNS.open do |dns| |         Resolv::DNS.open do |dns| | ||||||
|           dns.timeouts = 5 |           dns.timeouts = 5 | ||||||
| 
 | 
 | ||||||
|           addresses = dns.getaddresses(host).take(2) |           addresses = dns.getaddresses(host).take(2) | ||||||
|           time_slot = 10.0 / addresses.size |  | ||||||
| 
 | 
 | ||||||
|           addresses.each do |address| |           addresses.each do |address| | ||||||
|             begin |             begin | ||||||
|               raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) |               raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) | ||||||
| 
 | 
 | ||||||
|               ::Timeout.timeout(time_slot, HTTP::TimeoutError) do |               sock     = ::Socket.new(::Socket::AF_INET, ::Socket::SOCK_STREAM, 0) | ||||||
|                 return super(address.to_s, *args) |               sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s) | ||||||
|  | 
 | ||||||
|  |               sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) | ||||||
|  | 
 | ||||||
|  |               begin | ||||||
|  |                 sock.connect_nonblock(sockaddr) | ||||||
|  |               rescue IO::WaitWritable | ||||||
|  |                 if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect]) | ||||||
|  |                   begin | ||||||
|  |                     sock.connect_nonblock(sockaddr) | ||||||
|  |                   rescue Errno::EISCONN | ||||||
|  |                     # Yippee! | ||||||
|  |                   rescue | ||||||
|  |                     sock.close | ||||||
|  |                     raise | ||||||
|  |                   end | ||||||
|  |                 else | ||||||
|  |                   sock.close | ||||||
|  |                   raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds" | ||||||
|  |                 end | ||||||
|               end |               end | ||||||
|  | 
 | ||||||
|  |               return sock | ||||||
|             rescue => e |             rescue => e | ||||||
|               outer_e = e |               outer_e = e | ||||||
|             end |             end | ||||||
|  |  | ||||||
							
								
								
									
										114
									
								
								app/lib/request_pool.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								app/lib/request_pool.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,114 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require_relative './connection_pool/shared_connection_pool' | ||||||
|  | 
 | ||||||
|  | class RequestPool | ||||||
|  |   def self.current | ||||||
|  |     @current ||= RequestPool.new | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   class Reaper | ||||||
|  |     attr_reader :pool, :frequency | ||||||
|  | 
 | ||||||
|  |     def initialize(pool, frequency) | ||||||
|  |       @pool      = pool | ||||||
|  |       @frequency = frequency | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def run | ||||||
|  |       return unless frequency&.positive? | ||||||
|  | 
 | ||||||
|  |       Thread.new(frequency, pool) do |t, p| | ||||||
|  |         loop do | ||||||
|  |           sleep t | ||||||
|  |           p.flush | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   MAX_IDLE_TIME = 30 | ||||||
|  |   WAIT_TIMEOUT  = 5 | ||||||
|  |   MAX_POOL_SIZE = ENV.fetch('MAX_REQUEST_POOL_SIZE', 512).to_i | ||||||
|  | 
 | ||||||
|  |   class Connection | ||||||
|  |     attr_reader :site, :last_used_at, :created_at, :in_use, :dead, :fresh | ||||||
|  | 
 | ||||||
|  |     def initialize(site) | ||||||
|  |       @site         = site | ||||||
|  |       @http_client  = http_client | ||||||
|  |       @last_used_at = nil | ||||||
|  |       @created_at   = current_time | ||||||
|  |       @dead         = false | ||||||
|  |       @fresh        = true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def use | ||||||
|  |       @last_used_at = current_time | ||||||
|  |       @in_use       = true | ||||||
|  | 
 | ||||||
|  |       retries = 0 | ||||||
|  | 
 | ||||||
|  |       begin | ||||||
|  |         yield @http_client | ||||||
|  |       rescue HTTP::ConnectionError | ||||||
|  |         # It's possible the connection was closed, so let's | ||||||
|  |         # try re-opening it once | ||||||
|  | 
 | ||||||
|  |         close | ||||||
|  | 
 | ||||||
|  |         if @fresh || retries.positive? | ||||||
|  |           raise | ||||||
|  |         else | ||||||
|  |           @http_client = http_client | ||||||
|  |           retries     += 1 | ||||||
|  |           retry | ||||||
|  |         end | ||||||
|  |       rescue StandardError | ||||||
|  |         # If this connection raises errors of any kind, it's | ||||||
|  |         # better if it gets reaped as soon as possible | ||||||
|  | 
 | ||||||
|  |         close | ||||||
|  |         @dead = true | ||||||
|  |         raise | ||||||
|  |       end | ||||||
|  |     ensure | ||||||
|  |       @fresh  = false | ||||||
|  |       @in_use = false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def seconds_idle | ||||||
|  |       current_time - (@last_used_at || @created_at) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def close | ||||||
|  |       @http_client.close | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def http_client | ||||||
|  |       Request.http_client.persistent(@site, timeout: MAX_IDLE_TIME) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def current_time | ||||||
|  |       Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def initialize | ||||||
|  |     @pool   = ConnectionPool::SharedConnectionPool.new(size: MAX_POOL_SIZE, timeout: WAIT_TIMEOUT) { |site| Connection.new(site) } | ||||||
|  |     @reaper = Reaper.new(self, 30) | ||||||
|  |     @reaper.run | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def with(site, &block) | ||||||
|  |     @pool.with(site) do |connection| | ||||||
|  |       ActiveSupport::Notifications.instrument('with.request_pool', miss: connection.fresh, host: connection.site) do | ||||||
|  |         connection.use(&block) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   delegate :size, :flush, to: :@pool | ||||||
|  | end | ||||||
|  | @ -3,9 +3,11 @@ | ||||||
| class SidekiqErrorHandler | class SidekiqErrorHandler | ||||||
|   def call(*) |   def call(*) | ||||||
|     yield |     yield | ||||||
|   rescue Mastodon::HostValidationError => e |   rescue Mastodon::HostValidationError | ||||||
|     Rails.logger.error "#{e.class}: #{e.message}" |  | ||||||
|     Rails.logger.error e.backtrace.join("\n") |  | ||||||
|     # Do not retry |     # Do not retry | ||||||
|  |   ensure | ||||||
|  |     socket = Thread.current[:statsd_socket] | ||||||
|  |     socket&.close | ||||||
|  |     Thread.current[:statsd_socket] = nil | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -60,7 +60,9 @@ module Attachmentable | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def calculated_content_type(attachment) |   def calculated_content_type(attachment) | ||||||
|     Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp |     content_type = Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp | ||||||
|  |     content_type = 'video/mp4' if content_type == 'video/x-m4v' | ||||||
|  |     content_type | ||||||
|   rescue Terrapin::CommandLineError |   rescue Terrapin::CommandLineError | ||||||
|     '' |     '' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -35,6 +35,13 @@ class CustomFilter < ApplicationRecord | ||||||
|   before_validation :clean_up_contexts |   before_validation :clean_up_contexts | ||||||
|   after_commit :remove_cache |   after_commit :remove_cache | ||||||
| 
 | 
 | ||||||
|  |   def expires_in | ||||||
|  |     return @expires_in if defined?(@expires_in) | ||||||
|  |     return nil if expires_at.nil? | ||||||
|  | 
 | ||||||
|  |     [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def clean_up_contexts |   def clean_up_contexts | ||||||
|  |  | ||||||
|  | @ -15,6 +15,8 @@ class ActivityPub::ProcessAccountService < BaseService | ||||||
|     @domain      = domain |     @domain      = domain | ||||||
|     @collections = {} |     @collections = {} | ||||||
| 
 | 
 | ||||||
|  |     return if auto_suspend? | ||||||
|  | 
 | ||||||
|     RedisLock.acquire(lock_options) do |lock| |     RedisLock.acquire(lock_options) do |lock| | ||||||
|       if lock.acquired? |       if lock.acquired? | ||||||
|         @account        = Account.find_remote(@username, @domain) |         @account        = Account.find_remote(@username, @domain) | ||||||
|  | @ -55,7 +57,7 @@ class ActivityPub::ProcessAccountService < BaseService | ||||||
|     @account.domain       = @domain |     @account.domain       = @domain | ||||||
|     @account.private_key  = nil |     @account.private_key  = nil | ||||||
|     @account.suspended_at = domain_block.created_at if auto_suspend? |     @account.suspended_at = domain_block.created_at if auto_suspend? | ||||||
|     @account.silenced_at = domain_block.created_at if auto_silence? |     @account.silenced_at  = domain_block.created_at if auto_silence? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def update_account |   def update_account | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ class ResolveAccountService < BaseService | ||||||
|       return |       return | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     return if links_missing? |     return if links_missing? || auto_suspend? | ||||||
|     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) |     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) | ||||||
| 
 | 
 | ||||||
|     RedisLock.acquire(lock_options) do |lock| |     RedisLock.acquire(lock_options) do |lock| | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ class ActivityPub::DeliveryWorker | ||||||
|     @json           = json |     @json           = json | ||||||
|     @source_account = Account.find(source_account_id) |     @source_account = Account.find(source_account_id) | ||||||
|     @inbox_url      = inbox_url |     @inbox_url      = inbox_url | ||||||
|  |     @host           = Addressable::URI.parse(inbox_url).normalized_site | ||||||
| 
 | 
 | ||||||
|     perform_request |     perform_request | ||||||
| 
 | 
 | ||||||
|  | @ -28,16 +29,18 @@ class ActivityPub::DeliveryWorker | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def build_request |   def build_request(http_client) | ||||||
|     request = Request.new(:post, @inbox_url, body: @json) |     request = Request.new(:post, @inbox_url, body: @json, http_client: http_client) | ||||||
|     request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) |     request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) | ||||||
|     request.add_headers(HEADERS) |     request.add_headers(HEADERS) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def perform_request |   def perform_request | ||||||
|     light = Stoplight(@inbox_url) do |     light = Stoplight(@inbox_url) do | ||||||
|       build_request.perform do |response| |       request_pool.with(@host) do |http_client| | ||||||
|         raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) |         build_request(http_client).perform do |response| | ||||||
|  |           raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -51,10 +54,14 @@ class ActivityPub::DeliveryWorker | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def response_error_unsalvageable?(response) |   def response_error_unsalvageable?(response) | ||||||
|     (400...500).cover?(response.code) && ![401, 408, 429].include?(response.code) |     response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def failure_tracker |   def failure_tracker | ||||||
|     @failure_tracker ||= DeliveryFailureTracker.new(@inbox_url) |     @failure_tracker ||= DeliveryFailureTracker.new(@inbox_url) | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def request_pool | ||||||
|  |     RequestPool.current | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| instrumentation_hostname = ENV.fetch('INSTRUMENTATION_HOSTNAME') { 'localhost' } |  | ||||||
| 
 |  | ||||||
| ActiveSupport::Notifications.subscribe(/process_action.action_controller/) do |*args| |  | ||||||
|   event      = ActiveSupport::Notifications::Event.new(*args) |  | ||||||
|   controller = event.payload[:controller] |  | ||||||
|   action     = event.payload[:action] |  | ||||||
|   format     = event.payload[:format] || 'all' |  | ||||||
|   format     = 'all' if format == '*/*' |  | ||||||
|   status     = event.payload[:status] |  | ||||||
|   key        = "#{controller}.#{action}.#{format}.#{instrumentation_hostname}" |  | ||||||
| 
 |  | ||||||
|   ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.total_duration", value: event.duration |  | ||||||
|   ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.db_time", value: event.payload[:db_runtime] |  | ||||||
|   ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.view_time", value: event.payload[:view_runtime] |  | ||||||
|   ActiveSupport::Notifications.instrument :performance, measurement: "#{key}.status.#{status}" |  | ||||||
| end |  | ||||||
|  | @ -3,10 +3,10 @@ | ||||||
| if ENV['STATSD_ADDR'].present? | if ENV['STATSD_ADDR'].present? | ||||||
|   host, port = ENV['STATSD_ADDR'].split(':') |   host, port = ENV['STATSD_ADDR'].split(':') | ||||||
| 
 | 
 | ||||||
|   statsd = ::Statsd.new(host, port) |   $statsd = ::Statsd.new(host, port) | ||||||
|   statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } |   $statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } | ||||||
| 
 | 
 | ||||||
|   ::NSA.inform_statsd(statsd) do |informant| |   ::NSA.inform_statsd($statsd) do |informant| | ||||||
|     informant.collect(:action_controller, :web) |     informant.collect(:action_controller, :web) | ||||||
|     informant.collect(:active_record, :db) |     informant.collect(:active_record, :db) | ||||||
|     informant.collect(:active_support_cache, :cache) |     informant.collect(:active_support_cache, :cache) | ||||||
|  |  | ||||||
|  | @ -164,7 +164,7 @@ | ||||||
|     "webpack": "^4.34.0", |     "webpack": "^4.34.0", | ||||||
|     "webpack-assets-manifest": "^3.1.1", |     "webpack-assets-manifest": "^3.1.1", | ||||||
|     "webpack-bundle-analyzer": "^3.3.2", |     "webpack-bundle-analyzer": "^3.3.2", | ||||||
|     "webpack-cli": "^3.3.4", |     "webpack-cli": "^3.3.5", | ||||||
|     "webpack-merge": "^4.2.1", |     "webpack-merge": "^4.2.1", | ||||||
|     "websocket.js": "^0.1.12" |     "websocket.js": "^0.1.12" | ||||||
|   }, |   }, | ||||||
|  | @ -176,7 +176,7 @@ | ||||||
|     "eslint": "^5.16.0", |     "eslint": "^5.16.0", | ||||||
|     "eslint-plugin-import": "~2.17.3", |     "eslint-plugin-import": "~2.17.3", | ||||||
|     "eslint-plugin-jsx-a11y": "~6.2.1", |     "eslint-plugin-jsx-a11y": "~6.2.1", | ||||||
|     "eslint-plugin-promise": "~4.1.1", |     "eslint-plugin-promise": "~4.2.1", | ||||||
|     "eslint-plugin-react": "~7.12.1", |     "eslint-plugin-react": "~7.12.1", | ||||||
|     "jest": "^24.8.0", |     "jest": "^24.8.0", | ||||||
|     "raf": "^3.4.1", |     "raf": "^3.4.1", | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								spec/lib/connection_pool/shared_connection_pool_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								spec/lib/connection_pool/shared_connection_pool_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe ConnectionPool::SharedConnectionPool do | ||||||
|  |   class MiniConnection | ||||||
|  |     attr_reader :site | ||||||
|  | 
 | ||||||
|  |     def initialize(site) | ||||||
|  |       @site = site | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject { described_class.new(size: 5, timeout: 5) { |site| MiniConnection.new(site) } } | ||||||
|  | 
 | ||||||
|  |   describe '#with' do | ||||||
|  |     it 'runs a block with a connection' do | ||||||
|  |       block_run = false | ||||||
|  | 
 | ||||||
|  |       subject.with('foo') do |connection| | ||||||
|  |         expect(connection).to be_a MiniConnection | ||||||
|  |         block_run = true | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       expect(block_run).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										61
									
								
								spec/lib/connection_pool/shared_timed_stack_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								spec/lib/connection_pool/shared_timed_stack_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe ConnectionPool::SharedTimedStack do | ||||||
|  |   class MiniConnection | ||||||
|  |     attr_reader :site | ||||||
|  | 
 | ||||||
|  |     def initialize(site) | ||||||
|  |       @site = site | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject { described_class.new(5) { |site| MiniConnection.new(site) } } | ||||||
|  | 
 | ||||||
|  |   describe '#push' do | ||||||
|  |     it 'keeps the connection in the stack' do | ||||||
|  |       subject.push(MiniConnection.new('foo')) | ||||||
|  |       expect(subject.size).to eq 1 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#pop' do | ||||||
|  |     it 'returns a connection' do | ||||||
|  |       expect(subject.pop('foo')).to be_a MiniConnection | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns the same connection that was pushed in' do | ||||||
|  |       connection = MiniConnection.new('foo') | ||||||
|  |       subject.push(connection) | ||||||
|  |       expect(subject.pop('foo')).to be connection | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'does not create more than maximum amount of connections' do | ||||||
|  |       expect { 6.times { subject.pop('foo', 0) } }.to raise_error Timeout::Error | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'repurposes a connection for a different site when maximum amount is reached' do | ||||||
|  |       5.times { subject.push(MiniConnection.new('foo')) } | ||||||
|  |       expect(subject.pop('bar')).to be_a MiniConnection | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#empty?' do | ||||||
|  |     it 'returns true when no connections on the stack' do | ||||||
|  |       expect(subject.empty?).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns false when there are connections on the stack' do | ||||||
|  |       subject.push(MiniConnection.new('foo')) | ||||||
|  |       expect(subject.empty?).to be false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#size' do | ||||||
|  |     it 'returns the number of connections on the stack' do | ||||||
|  |       2.times { subject.push(MiniConnection.new('foo')) } | ||||||
|  |       expect(subject.size).to eq 2 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										63
									
								
								spec/lib/request_pool_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								spec/lib/request_pool_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe RequestPool do | ||||||
|  |   subject { described_class.new } | ||||||
|  | 
 | ||||||
|  |   describe '#with' do | ||||||
|  |     it 'returns a HTTP client for a host' do | ||||||
|  |       subject.with('http://example.com') do |http_client| | ||||||
|  |         expect(http_client).to be_a HTTP::Client | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns the same instance of HTTP client within the same thread for the same host' do | ||||||
|  |       test_client = nil | ||||||
|  | 
 | ||||||
|  |       subject.with('http://example.com') { |http_client| test_client = http_client } | ||||||
|  |       expect(test_client).to_not be_nil | ||||||
|  |       subject.with('http://example.com') { |http_client| expect(http_client).to be test_client } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns different HTTP clients for different hosts' do | ||||||
|  |       test_client = nil | ||||||
|  | 
 | ||||||
|  |       subject.with('http://example.com') { |http_client| test_client = http_client } | ||||||
|  |       expect(test_client).to_not be_nil | ||||||
|  |       subject.with('http://example.org') { |http_client| expect(http_client).to_not be test_client } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'grows to the number of threads accessing it' do | ||||||
|  |       stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') | ||||||
|  | 
 | ||||||
|  |       subject | ||||||
|  | 
 | ||||||
|  |       threads = 20.times.map do |i| | ||||||
|  |         Thread.new do | ||||||
|  |           20.times do | ||||||
|  |             subject.with('http://example.com') do |http_client| | ||||||
|  |               http_client.get('/').flush | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       threads.map(&:join) | ||||||
|  | 
 | ||||||
|  |       expect(subject.size).to be > 1 | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'closes idle connections' do | ||||||
|  |       stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') | ||||||
|  | 
 | ||||||
|  |       subject.with('http://example.com') do |http_client| | ||||||
|  |         http_client.get('/').flush | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       expect(subject.size).to eq 1 | ||||||
|  |       sleep RequestPool::MAX_IDLE_TIME + 30 + 1 | ||||||
|  |       expect(subject.size).to eq 0 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										185
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -2274,6 +2274,15 @@ caseless@~0.12.0: | ||||||
|   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" |   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" | ||||||
|   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= |   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= | ||||||
| 
 | 
 | ||||||
|  | chalk@2.4.2, chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: | ||||||
|  |   version "2.4.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" | ||||||
|  |   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== | ||||||
|  |   dependencies: | ||||||
|  |     ansi-styles "^3.2.1" | ||||||
|  |     escape-string-regexp "^1.0.5" | ||||||
|  |     supports-color "^5.3.0" | ||||||
|  | 
 | ||||||
| chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: | chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: | ||||||
|   version "1.1.3" |   version "1.1.3" | ||||||
|   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" |   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" | ||||||
|  | @ -2285,15 +2294,6 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: | ||||||
|     strip-ansi "^3.0.0" |     strip-ansi "^3.0.0" | ||||||
|     supports-color "^2.0.0" |     supports-color "^2.0.0" | ||||||
| 
 | 
 | ||||||
| chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: |  | ||||||
|   version "2.4.2" |  | ||||||
|   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" |  | ||||||
|   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== |  | ||||||
|   dependencies: |  | ||||||
|     ansi-styles "^3.2.1" |  | ||||||
|     escape-string-regexp "^1.0.5" |  | ||||||
|     supports-color "^5.3.0" |  | ||||||
| 
 |  | ||||||
| chardet@^0.7.0: | chardet@^0.7.0: | ||||||
|   version "0.7.0" |   version "0.7.0" | ||||||
|   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" |   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" | ||||||
|  | @ -2408,6 +2408,15 @@ cliui@^4.0.0: | ||||||
|     strip-ansi "^4.0.0" |     strip-ansi "^4.0.0" | ||||||
|     wrap-ansi "^2.0.0" |     wrap-ansi "^2.0.0" | ||||||
| 
 | 
 | ||||||
|  | cliui@^5.0.0: | ||||||
|  |   version "5.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" | ||||||
|  |   integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== | ||||||
|  |   dependencies: | ||||||
|  |     string-width "^3.1.0" | ||||||
|  |     strip-ansi "^5.2.0" | ||||||
|  |     wrap-ansi "^5.1.0" | ||||||
|  | 
 | ||||||
| clone-deep@^2.0.1: | clone-deep@^2.0.1: | ||||||
|   version "2.0.2" |   version "2.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" |   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" | ||||||
|  | @ -2741,7 +2750,7 @@ cross-env@^5.1.4: | ||||||
|     cross-spawn "^6.0.5" |     cross-spawn "^6.0.5" | ||||||
|     is-windows "^1.0.0" |     is-windows "^1.0.0" | ||||||
| 
 | 
 | ||||||
| cross-spawn@^6.0.0, cross-spawn@^6.0.5: | cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: | ||||||
|   version "6.0.5" |   version "6.0.5" | ||||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" |   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" | ||||||
|   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== |   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== | ||||||
|  | @ -3428,7 +3437,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: | ||||||
|   dependencies: |   dependencies: | ||||||
|     once "^1.4.0" |     once "^1.4.0" | ||||||
| 
 | 
 | ||||||
| enhanced-resolve@^4.1.0: | enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0: | ||||||
|   version "4.1.0" |   version "4.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" |   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" | ||||||
|   integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== |   integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== | ||||||
|  | @ -3688,10 +3697,10 @@ eslint-plugin-jsx-a11y@~6.2.1: | ||||||
|     has "^1.0.3" |     has "^1.0.3" | ||||||
|     jsx-ast-utils "^2.0.1" |     jsx-ast-utils "^2.0.1" | ||||||
| 
 | 
 | ||||||
| eslint-plugin-promise@~4.1.1: | eslint-plugin-promise@~4.2.1: | ||||||
|   version "4.1.1" |   version "4.2.1" | ||||||
|   resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" |   resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" | ||||||
|   integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ== |   integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== | ||||||
| 
 | 
 | ||||||
| eslint-plugin-react@~7.12.1: | eslint-plugin-react@~7.12.1: | ||||||
|   version "7.12.1" |   version "7.12.1" | ||||||
|  | @ -4208,13 +4217,13 @@ find-up@^3.0.0: | ||||||
|   dependencies: |   dependencies: | ||||||
|     locate-path "^3.0.0" |     locate-path "^3.0.0" | ||||||
| 
 | 
 | ||||||
| findup-sync@^2.0.0: | findup-sync@3.0.0: | ||||||
|   version "2.0.0" |   version "3.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" |   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" | ||||||
|   integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= |   integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== | ||||||
|   dependencies: |   dependencies: | ||||||
|     detect-file "^1.0.0" |     detect-file "^1.0.0" | ||||||
|     is-glob "^3.1.0" |     is-glob "^4.0.0" | ||||||
|     micromatch "^3.0.4" |     micromatch "^3.0.4" | ||||||
|     resolve-dir "^1.0.1" |     resolve-dir "^1.0.1" | ||||||
| 
 | 
 | ||||||
|  | @ -4437,6 +4446,11 @@ get-caller-file@^1.0.1: | ||||||
|   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" |   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" | ||||||
|   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== |   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== | ||||||
| 
 | 
 | ||||||
|  | get-caller-file@^2.0.1: | ||||||
|  |   version "2.0.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" | ||||||
|  |   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== | ||||||
|  | 
 | ||||||
| get-stream@^4.0.0: | get-stream@^4.0.0: | ||||||
|   version "4.1.0" |   version "4.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" |   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" | ||||||
|  | @ -4476,6 +4490,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: | ||||||
|     once "^1.3.0" |     once "^1.3.0" | ||||||
|     path-is-absolute "^1.0.0" |     path-is-absolute "^1.0.0" | ||||||
| 
 | 
 | ||||||
|  | global-modules@2.0.0: | ||||||
|  |   version "2.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" | ||||||
|  |   integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== | ||||||
|  |   dependencies: | ||||||
|  |     global-prefix "^3.0.0" | ||||||
|  | 
 | ||||||
| global-modules@^1.0.0: | global-modules@^1.0.0: | ||||||
|   version "1.0.0" |   version "1.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" |   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" | ||||||
|  | @ -4496,6 +4517,15 @@ global-prefix@^1.0.1: | ||||||
|     is-windows "^1.0.1" |     is-windows "^1.0.1" | ||||||
|     which "^1.2.14" |     which "^1.2.14" | ||||||
| 
 | 
 | ||||||
|  | global-prefix@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" | ||||||
|  |   integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== | ||||||
|  |   dependencies: | ||||||
|  |     ini "^1.3.5" | ||||||
|  |     kind-of "^6.0.2" | ||||||
|  |     which "^1.3.1" | ||||||
|  | 
 | ||||||
| globals@^11.1.0, globals@^11.7.0: | globals@^11.1.0, globals@^11.7.0: | ||||||
|   version "11.12.0" |   version "11.12.0" | ||||||
|   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" |   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" | ||||||
|  | @ -4921,7 +4951,7 @@ import-from@^2.1.0: | ||||||
|   dependencies: |   dependencies: | ||||||
|     resolve-from "^3.0.0" |     resolve-from "^3.0.0" | ||||||
| 
 | 
 | ||||||
| import-local@^2.0.0: | import-local@2.0.0, import-local@^2.0.0: | ||||||
|   version "2.0.0" |   version "2.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" |   resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" | ||||||
|   integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== |   integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== | ||||||
|  | @ -4970,7 +5000,7 @@ inherits@2.0.1: | ||||||
|   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" |   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" | ||||||
|   integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= |   integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= | ||||||
| 
 | 
 | ||||||
| ini@^1.3.4, ini@~1.3.0: | ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: | ||||||
|   version "1.3.5" |   version "1.3.5" | ||||||
|   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" |   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" | ||||||
|   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== |   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== | ||||||
|  | @ -5021,7 +5051,7 @@ internal-ip@^4.3.0: | ||||||
|     default-gateway "^4.2.0" |     default-gateway "^4.2.0" | ||||||
|     ipaddr.js "^1.9.0" |     ipaddr.js "^1.9.0" | ||||||
| 
 | 
 | ||||||
| interpret@^1.1.0: | interpret@1.2.0: | ||||||
|   version "1.2.0" |   version "1.2.0" | ||||||
|   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" |   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" | ||||||
|   integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== |   integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== | ||||||
|  | @ -6119,7 +6149,7 @@ loader-utils@0.2.x: | ||||||
|     json5 "^0.5.0" |     json5 "^0.5.0" | ||||||
|     object-assign "^4.0.1" |     object-assign "^4.0.1" | ||||||
| 
 | 
 | ||||||
| loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3: | loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3: | ||||||
|   version "1.2.3" |   version "1.2.3" | ||||||
|   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" |   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" | ||||||
|   integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== |   integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== | ||||||
|  | @ -7034,7 +7064,7 @@ os-homedir@^1.0.0: | ||||||
|   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" |   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" | ||||||
|   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= |   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= | ||||||
| 
 | 
 | ||||||
| os-locale@^3.0.0: | os-locale@^3.0.0, os-locale@^3.1.0: | ||||||
|   version "3.1.0" |   version "3.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" |   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" | ||||||
|   integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== |   integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== | ||||||
|  | @ -7831,11 +7861,6 @@ prepend-http@^1.0.0: | ||||||
|   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" |   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" | ||||||
|   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= |   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= | ||||||
| 
 | 
 | ||||||
| prettier@^1.17.0: |  | ||||||
|   version "1.18.2" |  | ||||||
|   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" |  | ||||||
|   integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== |  | ||||||
| 
 |  | ||||||
| pretty-format@^24.8.0: | pretty-format@^24.8.0: | ||||||
|   version "24.8.0" |   version "24.8.0" | ||||||
|   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" |   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" | ||||||
|  | @ -8650,6 +8675,11 @@ require-main-filename@^1.0.1: | ||||||
|   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" |   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" | ||||||
|   integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= |   integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= | ||||||
| 
 | 
 | ||||||
|  | require-main-filename@^2.0.0: | ||||||
|  |   version "2.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" | ||||||
|  |   integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== | ||||||
|  | 
 | ||||||
| require-package-name@^2.0.1: | require-package-name@^2.0.1: | ||||||
|   version "2.0.1" |   version "2.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9" |   resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9" | ||||||
|  | @ -9411,7 +9441,7 @@ string-width@^1.0.1: | ||||||
|     is-fullwidth-code-point "^2.0.0" |     is-fullwidth-code-point "^2.0.0" | ||||||
|     strip-ansi "^4.0.0" |     strip-ansi "^4.0.0" | ||||||
| 
 | 
 | ||||||
| string-width@^3.0.0: | string-width@^3.0.0, string-width@^3.1.0: | ||||||
|   version "3.1.0" |   version "3.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" |   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" | ||||||
|   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== |   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== | ||||||
|  | @ -9464,7 +9494,7 @@ strip-ansi@^4.0.0: | ||||||
|   dependencies: |   dependencies: | ||||||
|     ansi-regex "^3.0.0" |     ansi-regex "^3.0.0" | ||||||
| 
 | 
 | ||||||
| strip-ansi@^5.0.0, strip-ansi@^5.1.0: | strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: | ||||||
|   version "5.2.0" |   version "5.2.0" | ||||||
|   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" |   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" | ||||||
|   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== |   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== | ||||||
|  | @ -9515,6 +9545,13 @@ substring-trie@^1.0.2: | ||||||
|   resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.2.tgz#7b42592391628b4f2cb17365c6cce4257c7b7af5" |   resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.2.tgz#7b42592391628b4f2cb17365c6cce4257c7b7af5" | ||||||
|   integrity sha1-e0JZI5Fii08ssXNlxszkJXx7evU= |   integrity sha1-e0JZI5Fii08ssXNlxszkJXx7evU= | ||||||
| 
 | 
 | ||||||
|  | supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0: | ||||||
|  |   version "6.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" | ||||||
|  |   integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== | ||||||
|  |   dependencies: | ||||||
|  |     has-flag "^3.0.0" | ||||||
|  | 
 | ||||||
| supports-color@^2.0.0: | supports-color@^2.0.0: | ||||||
|   version "2.0.0" |   version "2.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" |   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" | ||||||
|  | @ -9527,20 +9564,13 @@ supports-color@^3.2.3: | ||||||
|   dependencies: |   dependencies: | ||||||
|     has-flag "^1.0.0" |     has-flag "^1.0.0" | ||||||
| 
 | 
 | ||||||
| supports-color@^5.3.0, supports-color@^5.5.0: | supports-color@^5.3.0: | ||||||
|   version "5.5.0" |   version "5.5.0" | ||||||
|   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" |   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" | ||||||
|   integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== |   integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== | ||||||
|   dependencies: |   dependencies: | ||||||
|     has-flag "^3.0.0" |     has-flag "^3.0.0" | ||||||
| 
 | 
 | ||||||
| supports-color@^6.0.0, supports-color@^6.1.0: |  | ||||||
|   version "6.1.0" |  | ||||||
|   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" |  | ||||||
|   integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== |  | ||||||
|   dependencies: |  | ||||||
|     has-flag "^3.0.0" |  | ||||||
| 
 |  | ||||||
| svgo@^1.0.0: | svgo@^1.0.0: | ||||||
|   version "1.1.1" |   version "1.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" |   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" | ||||||
|  | @ -10017,10 +10047,10 @@ uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: | ||||||
|   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" |   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" | ||||||
|   integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== |   integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== | ||||||
| 
 | 
 | ||||||
| v8-compile-cache@^2.0.2: | v8-compile-cache@2.0.3: | ||||||
|   version "2.0.2" |   version "2.0.3" | ||||||
|   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" |   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" | ||||||
|   integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== |   integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== | ||||||
| 
 | 
 | ||||||
| validate-npm-package-license@^3.0.1: | validate-npm-package-license@^3.0.1: | ||||||
|   version "3.0.4" |   version "3.0.4" | ||||||
|  | @ -10142,23 +10172,22 @@ webpack-bundle-analyzer@^3.3.2: | ||||||
|     opener "^1.5.1" |     opener "^1.5.1" | ||||||
|     ws "^6.0.0" |     ws "^6.0.0" | ||||||
| 
 | 
 | ||||||
| webpack-cli@^3.3.4: | webpack-cli@^3.3.5: | ||||||
|   version "3.3.4" |   version "3.3.5" | ||||||
|   resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.4.tgz#de27e281c48a897b8c219cb093e261d5f6afe44a" |   resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.5.tgz#f4d1238a66a2843d9cebf189835ea22142e72767" | ||||||
|   integrity sha512-ubJGQEKMtBSpT+LiL5hXvn2GIOWiRWItR1DGUqJRhwRBeGhpRXjvF5f0erqdRJLErkfqS5/Ldkkedh4AL5Q1ZQ== |   integrity sha512-w0j/s42c5UhchwTmV/45MLQnTVwRoaUTu9fM5LuyOd/8lFoCNCELDogFoecx5NzRUndO0yD/gF2b02XKMnmAWQ== | ||||||
|   dependencies: |   dependencies: | ||||||
|     chalk "^2.4.1" |     chalk "2.4.2" | ||||||
|     cross-spawn "^6.0.5" |     cross-spawn "6.0.5" | ||||||
|     enhanced-resolve "^4.1.0" |     enhanced-resolve "4.1.0" | ||||||
|     findup-sync "^2.0.0" |     findup-sync "3.0.0" | ||||||
|     global-modules "^1.0.0" |     global-modules "2.0.0" | ||||||
|     import-local "^2.0.0" |     import-local "2.0.0" | ||||||
|     interpret "^1.1.0" |     interpret "1.2.0" | ||||||
|     loader-utils "^1.1.0" |     loader-utils "1.2.3" | ||||||
|     prettier "^1.17.0" |     supports-color "6.1.0" | ||||||
|     supports-color "^5.5.0" |     v8-compile-cache "2.0.3" | ||||||
|     v8-compile-cache "^2.0.2" |     yargs "13.2.4" | ||||||
|     yargs "^12.0.5" |  | ||||||
| 
 | 
 | ||||||
| webpack-dev-middleware@^3.7.0: | webpack-dev-middleware@^3.7.0: | ||||||
|   version "3.7.0" |   version "3.7.0" | ||||||
|  | @ -10320,7 +10349,7 @@ which-module@^2.0.0: | ||||||
|   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" | ||||||
|   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= |   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= | ||||||
| 
 | 
 | ||||||
| which@^1.2.14, which@^1.2.9, which@^1.3.0: | which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: | ||||||
|   version "1.3.1" |   version "1.3.1" | ||||||
|   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" |   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" | ||||||
|   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== |   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== | ||||||
|  | @ -10359,6 +10388,15 @@ wrap-ansi@^2.0.0: | ||||||
|     string-width "^1.0.1" |     string-width "^1.0.1" | ||||||
|     strip-ansi "^3.0.1" |     strip-ansi "^3.0.1" | ||||||
| 
 | 
 | ||||||
|  | wrap-ansi@^5.1.0: | ||||||
|  |   version "5.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" | ||||||
|  |   integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== | ||||||
|  |   dependencies: | ||||||
|  |     ansi-styles "^3.2.0" | ||||||
|  |     string-width "^3.0.0" | ||||||
|  |     strip-ansi "^5.0.0" | ||||||
|  | 
 | ||||||
| wrappy@1: | wrappy@1: | ||||||
|   version "1.0.2" |   version "1.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" |   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" | ||||||
|  | @ -10429,6 +10467,14 @@ yargs-parser@^11.1.1: | ||||||
|     camelcase "^5.0.0" |     camelcase "^5.0.0" | ||||||
|     decamelize "^1.2.0" |     decamelize "^1.2.0" | ||||||
| 
 | 
 | ||||||
|  | yargs-parser@^13.1.0: | ||||||
|  |   version "13.1.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" | ||||||
|  |   integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== | ||||||
|  |   dependencies: | ||||||
|  |     camelcase "^5.0.0" | ||||||
|  |     decamelize "^1.2.0" | ||||||
|  | 
 | ||||||
| yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5: | yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5: | ||||||
|   version "12.0.5" |   version "12.0.5" | ||||||
|   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" |   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" | ||||||
|  | @ -10446,3 +10492,20 @@ yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5: | ||||||
|     which-module "^2.0.0" |     which-module "^2.0.0" | ||||||
|     y18n "^3.2.1 || ^4.0.0" |     y18n "^3.2.1 || ^4.0.0" | ||||||
|     yargs-parser "^11.1.1" |     yargs-parser "^11.1.1" | ||||||
|  | 
 | ||||||
|  | yargs@13.2.4: | ||||||
|  |   version "13.2.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" | ||||||
|  |   integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== | ||||||
|  |   dependencies: | ||||||
|  |     cliui "^5.0.0" | ||||||
|  |     find-up "^3.0.0" | ||||||
|  |     get-caller-file "^2.0.1" | ||||||
|  |     os-locale "^3.1.0" | ||||||
|  |     require-directory "^2.1.1" | ||||||
|  |     require-main-filename "^2.0.0" | ||||||
|  |     set-blocking "^2.0.0" | ||||||
|  |     string-width "^3.0.0" | ||||||
|  |     which-module "^2.0.0" | ||||||
|  |     y18n "^4.0.0" | ||||||
|  |     yargs-parser "^13.1.0" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue