redisse 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.gitignore +19 -0
 - data/.rspec +3 -0
 - data/.ruby-version +1 -0
 - data/.travis.yml +3 -0
 - data/Gemfile +4 -0
 - data/LICENSE.txt +22 -0
 - data/README.md +152 -0
 - data/Rakefile +28 -0
 - data/bin/redisse +4 -0
 - data/example/.env +3 -0
 - data/example/README.md +37 -0
 - data/example/bin/publish +15 -0
 - data/example/bin/redis +13 -0
 - data/example/config.ru +40 -0
 - data/example/nginx.conf +29 -0
 - data/example/public/index.html +46 -0
 - data/example/spec/app_spec.rb +39 -0
 - data/lib/redisse.rb +181 -0
 - data/lib/redisse/configuration.rb +47 -0
 - data/lib/redisse/publisher.rb +70 -0
 - data/lib/redisse/redirect_endpoint.rb +45 -0
 - data/lib/redisse/server.rb +205 -0
 - data/lib/redisse/server/redis.rb +39 -0
 - data/lib/redisse/server/responses.rb +22 -0
 - data/lib/redisse/server/stats.rb +10 -0
 - data/lib/redisse/server_sent_events.rb +17 -0
 - data/lib/redisse/version.rb +3 -0
 - data/redisse.gemspec +29 -0
 - data/spec/example_spec.rb +200 -0
 - data/spec/publisher_spec.rb +81 -0
 - data/spec/redirect_endpoint_spec.rb +98 -0
 - data/spec/server_sent_events_spec.rb +53 -0
 - data/spec/spec_helper.rb +7 -0
 - data/spec/spec_system_helper.rb +204 -0
 - metadata +214 -0
 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'rack/test'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            $app, _opts = Rack::Builder.parse_file __dir__ + '/../config.ru'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            describe "Example App" do
         
     | 
| 
      
 6 
     | 
    
         
            +
              include Rack::Test::Methods
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def app
         
     | 
| 
      
 9 
     | 
    
         
            +
                $app
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              describe "/publish" do
         
     | 
| 
      
 13 
     | 
    
         
            +
                context "basic" do
         
     | 
| 
      
 14 
     | 
    
         
            +
                  before do
         
     | 
| 
      
 15 
     | 
    
         
            +
                    Redisse.test_mode!
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  it "publishes the message to the channel" do
         
     | 
| 
      
 19 
     | 
    
         
            +
                    post "/publish", channel: 'global', message: 'Hello'
         
     | 
| 
      
 20 
     | 
    
         
            +
                    expect(Redisse.published.size).to be == 1
         
     | 
| 
      
 21 
     | 
    
         
            +
                    event = Redisse.published.first
         
     | 
| 
      
 22 
     | 
    
         
            +
                    expect(event.channel).to be == 'global'
         
     | 
| 
      
 23 
     | 
    
         
            +
                    expect(event.type).to be == :message
         
     | 
| 
      
 24 
     | 
    
         
            +
                    expect(event.data).to be == 'Hello'
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                context "filtered" do
         
     | 
| 
      
 29 
     | 
    
         
            +
                  before do
         
     | 
| 
      
 30 
     | 
    
         
            +
                    Redisse.test_filter = :unused_type
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  it "publishes the message with the 'message' type" do
         
     | 
| 
      
 34 
     | 
    
         
            +
                    post "/publish", channel: 'global', message: 'Hello'
         
     | 
| 
      
 35 
     | 
    
         
            +
                    expect(Redisse.published.size).to be == 0
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/redisse.rb
    ADDED
    
    | 
         @@ -0,0 +1,181 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'redisse/version'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'redisse/publisher'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'redis'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            # Public: A HTTP API to serve Server-Sent Events via a Redis backend.
         
     | 
| 
      
 6 
     | 
    
         
            +
            module Redisse
         
     | 
| 
      
 7 
     | 
    
         
            +
              # Public: Gets/Sets the String URL of the Redis server to connect to.
         
     | 
| 
      
 8 
     | 
    
         
            +
              #
         
     | 
| 
      
 9 
     | 
    
         
            +
              # Note that while the Redis pubsub mechanism works outside of the Redis key
         
     | 
| 
      
 10 
     | 
    
         
            +
              # namespace and ignores the database (the path part of the URL), the
         
     | 
| 
      
 11 
     | 
    
         
            +
              # database will still be used to store an history of the events sent to
         
     | 
| 
      
 12 
     | 
    
         
            +
              # support Last-Event-Id.
         
     | 
| 
      
 13 
     | 
    
         
            +
              #
         
     | 
| 
      
 14 
     | 
    
         
            +
              # Defaults to the REDISSE_REDIS environment variable and if it is not set, to
         
     | 
| 
      
 15 
     | 
    
         
            +
              # redis://localhost:6379/.
         
     | 
| 
      
 16 
     | 
    
         
            +
              attr_accessor :redis_server
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # Public: The port on which the server listens.
         
     | 
| 
      
 19 
     | 
    
         
            +
              #
         
     | 
| 
      
 20 
     | 
    
         
            +
              # Defaults to the REDISSE_PORT environment variable and if it is not set, to
         
     | 
| 
      
 21 
     | 
    
         
            +
              # 8080.
         
     | 
| 
      
 22 
     | 
    
         
            +
              attr_accessor :default_port
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
              # Public: The internal URL hierarchy to redirect to with X-Accel-Redirect.
         
     | 
| 
      
 25 
     | 
    
         
            +
              #
         
     | 
| 
      
 26 
     | 
    
         
            +
              # When this property is set, Redisse will work totally differently. Your Ruby
         
     | 
| 
      
 27 
     | 
    
         
            +
              # code will not be loaded by the events server itself, but only by the
         
     | 
| 
      
 28 
     | 
    
         
            +
              # {#redirect_endpoint} Rack app that you will have to route to in your Rack
         
     | 
| 
      
 29 
     | 
    
         
            +
              # app (e.g. using +map+ in +config.ru+) and this endpoint will redirect to
         
     | 
| 
      
 30 
     | 
    
         
            +
              # this internal URL hierarchy.
         
     | 
| 
      
 31 
     | 
    
         
            +
              #
         
     | 
| 
      
 32 
     | 
    
         
            +
              # Defaults to /redisse.
         
     | 
| 
      
 33 
     | 
    
         
            +
              attr_accessor :nginx_internal_url
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              # Public: Send an event to subscribers, of the given type.
         
     | 
| 
      
 36 
     | 
    
         
            +
              #
         
     | 
| 
      
 37 
     | 
    
         
            +
              # All browsers subscribing to the events server will receive a Server-Sent
         
     | 
| 
      
 38 
     | 
    
         
            +
              # Event of the chosen type.
         
     | 
| 
      
 39 
     | 
    
         
            +
              #
         
     | 
| 
      
 40 
     | 
    
         
            +
              # channel      - The channel to publish the message to.
         
     | 
| 
      
 41 
     | 
    
         
            +
              # type_message - The type of the event and the content of the message, as a
         
     | 
| 
      
 42 
     | 
    
         
            +
              #                Hash of form { type => message } or simply the message as
         
     | 
| 
      
 43 
     | 
    
         
            +
              #                a String, for the default event type :message.
         
     | 
| 
      
 44 
     | 
    
         
            +
              #
         
     | 
| 
      
 45 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 46 
     | 
    
         
            +
              #
         
     | 
| 
      
 47 
     | 
    
         
            +
              #   Redisse.publish(:global, notice: 'This is a server-sent event.')
         
     | 
| 
      
 48 
     | 
    
         
            +
              #   Redisse.publish(:global, 'Hello, World!')
         
     | 
| 
      
 49 
     | 
    
         
            +
              #
         
     | 
| 
      
 50 
     | 
    
         
            +
              #   # on the browser side:
         
     | 
| 
      
 51 
     | 
    
         
            +
              #   var source = new EventSource(eventsURL);
         
     | 
| 
      
 52 
     | 
    
         
            +
              #   source.addEventListener('notice', function(e) {
         
     | 
| 
      
 53 
     | 
    
         
            +
              #     console.log(e.data) // logs 'This is a server-sent event.'
         
     | 
| 
      
 54 
     | 
    
         
            +
              #   }, false)
         
     | 
| 
      
 55 
     | 
    
         
            +
              #   source.addEventListener('message', function(e) {
         
     | 
| 
      
 56 
     | 
    
         
            +
              #     console.log(e.data) // logs 'Hello, World!'
         
     | 
| 
      
 57 
     | 
    
         
            +
              #   }, false)
         
     | 
| 
      
 58 
     | 
    
         
            +
              def publish(channel, message)
         
     | 
| 
      
 59 
     | 
    
         
            +
                type, message = Hash(message).first if message.respond_to?(:to_h)
         
     | 
| 
      
 60 
     | 
    
         
            +
                type ||= :message
         
     | 
| 
      
 61 
     | 
    
         
            +
                publisher.publish(channel, message, type)
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
              # Public: The list of channels to subscribe to.
         
     | 
| 
      
 65 
     | 
    
         
            +
              #
         
     | 
| 
      
 66 
     | 
    
         
            +
              # Once {Redisse.channels} has been called, the given block is this method.
         
     | 
| 
      
 67 
     | 
    
         
            +
              # The block must satisfy this interface:
         
     | 
| 
      
 68 
     | 
    
         
            +
              #
         
     | 
| 
      
 69 
     | 
    
         
            +
              # env - The Rack environment for this request.
         
     | 
| 
      
 70 
     | 
    
         
            +
              #
         
     | 
| 
      
 71 
     | 
    
         
            +
              # Returns an Array of String naming the channels to subscribe to.
         
     | 
| 
      
 72 
     | 
    
         
            +
              #
         
     | 
| 
      
 73 
     | 
    
         
            +
              # Raises NotImplementedError unless {Redisse.channels} has been called.
         
     | 
| 
      
 74 
     | 
    
         
            +
              def channels(env)
         
     | 
| 
      
 75 
     | 
    
         
            +
                raise NotImplementedError, "you must call Redisse.channels first"
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              # Public: Use test mode.
         
     | 
| 
      
 79 
     | 
    
         
            +
              #
         
     | 
| 
      
 80 
     | 
    
         
            +
              # Instead of actually publishing to Redis, events will be stored in
         
     | 
| 
      
 81 
     | 
    
         
            +
              # {#published} to use for tests.
         
     | 
| 
      
 82 
     | 
    
         
            +
              #
         
     | 
| 
      
 83 
     | 
    
         
            +
              # Must be called before each test in order for published events to be
         
     | 
| 
      
 84 
     | 
    
         
            +
              # emptied.
         
     | 
| 
      
 85 
     | 
    
         
            +
              #
         
     | 
| 
      
 86 
     | 
    
         
            +
              # See also {#test_filter=}.
         
     | 
| 
      
 87 
     | 
    
         
            +
              #
         
     | 
| 
      
 88 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 89 
     | 
    
         
            +
              #
         
     | 
| 
      
 90 
     | 
    
         
            +
              #   # RSpec
         
     | 
| 
      
 91 
     | 
    
         
            +
              #   before { Redisse.test_mode! }
         
     | 
| 
      
 92 
     | 
    
         
            +
              def test_mode!
         
     | 
| 
      
 93 
     | 
    
         
            +
                @publisher = TestPublisher.new
         
     | 
| 
      
 94 
     | 
    
         
            +
              end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
              # Public: Filter events stored in test mode.
         
     | 
| 
      
 97 
     | 
    
         
            +
              #
         
     | 
| 
      
 98 
     | 
    
         
            +
              # If set, only events whose type match with the filter are stored in
         
     | 
| 
      
 99 
     | 
    
         
            +
              # {#published}. A filter matches by using case equality, which allows using
         
     | 
| 
      
 100 
     | 
    
         
            +
              # a simple Symbol or a Proc for more advanced filters:
         
     | 
| 
      
 101 
     | 
    
         
            +
              #
         
     | 
| 
      
 102 
     | 
    
         
            +
              # Automatically sets {#test_mode!}, so it also clears the previous events.
         
     | 
| 
      
 103 
     | 
    
         
            +
              #
         
     | 
| 
      
 104 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 105 
     | 
    
         
            +
              #
         
     | 
| 
      
 106 
     | 
    
         
            +
              #   Redisse.test_filter = -> type { %i(foo baz).include? type }
         
     | 
| 
      
 107 
     | 
    
         
            +
              #   Redisse.publish :global, foo: 'stored'
         
     | 
| 
      
 108 
     | 
    
         
            +
              #   Redisse.publish :global, bar: 'skipped'
         
     | 
| 
      
 109 
     | 
    
         
            +
              #   Redisse.publish :global, baz: 'stored'
         
     | 
| 
      
 110 
     | 
    
         
            +
              #   Redisse.published.size # => 2
         
     | 
| 
      
 111 
     | 
    
         
            +
              def test_filter=(filter)
         
     | 
| 
      
 112 
     | 
    
         
            +
                test_mode!
         
     | 
| 
      
 113 
     | 
    
         
            +
                publisher.filter = filter
         
     | 
| 
      
 114 
     | 
    
         
            +
              end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
              # Public: Returns the published events.
         
     | 
| 
      
 117 
     | 
    
         
            +
              #
         
     | 
| 
      
 118 
     | 
    
         
            +
              # Fails unless {#test_mode!} is set.
         
     | 
| 
      
 119 
     | 
    
         
            +
              def published
         
     | 
| 
      
 120 
     | 
    
         
            +
                fail "Call #{self}.test_mode! first" unless publisher.respond_to?(:published)
         
     | 
| 
      
 121 
     | 
    
         
            +
                publisher.published
         
     | 
| 
      
 122 
     | 
    
         
            +
              end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
              # Internal: List of middlewares defined with {#use}.
         
     | 
| 
      
 125 
     | 
    
         
            +
              #
         
     | 
| 
      
 126 
     | 
    
         
            +
              # Used by Goliath to build the server.
         
     | 
| 
      
 127 
     | 
    
         
            +
              def middlewares
         
     | 
| 
      
 128 
     | 
    
         
            +
                @middlewares ||= []
         
     | 
| 
      
 129 
     | 
    
         
            +
              end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
              # Public: Define a middleware for the server.
         
     | 
| 
      
 132 
     | 
    
         
            +
              #
         
     | 
| 
      
 133 
     | 
    
         
            +
              # See {https://github.com/postrank-labs/goliath/wiki/Middleware Goliath middlewares}.
         
     | 
| 
      
 134 
     | 
    
         
            +
              #
         
     | 
| 
      
 135 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 136 
     | 
    
         
            +
              #
         
     | 
| 
      
 137 
     | 
    
         
            +
              #    Redisse.use MyMiddleware, foo: true
         
     | 
| 
      
 138 
     | 
    
         
            +
              def use(middleware, *args, &block)
         
     | 
| 
      
 139 
     | 
    
         
            +
                middlewares << [middleware, args, block]
         
     | 
| 
      
 140 
     | 
    
         
            +
              end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
              # Public: Define a Goliath plugin to run with the server.
         
     | 
| 
      
 143 
     | 
    
         
            +
              #
         
     | 
| 
      
 144 
     | 
    
         
            +
              # See {https://github.com/postrank-labs/goliath/wiki/Plugins Goliath plugins}.
         
     | 
| 
      
 145 
     | 
    
         
            +
              def plugin(name, *args)
         
     | 
| 
      
 146 
     | 
    
         
            +
                plugins << [name, args]
         
     | 
| 
      
 147 
     | 
    
         
            +
              end
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
              # Public: The Rack application that redirects to {#nginx_internal_url}.
         
     | 
| 
      
 150 
     | 
    
         
            +
              #
         
     | 
| 
      
 151 
     | 
    
         
            +
              # If you set {#nginx_internal_url}, you need to call this Rack application
         
     | 
| 
      
 152 
     | 
    
         
            +
              # to redirect to the Redisse server.
         
     | 
| 
      
 153 
     | 
    
         
            +
              #
         
     | 
| 
      
 154 
     | 
    
         
            +
              # Also note that when using the redirect endpoint, two channel names are
         
     | 
| 
      
 155 
     | 
    
         
            +
              # reserved, and cannot be used: +polling+ and +lastEventId+.
         
     | 
| 
      
 156 
     | 
    
         
            +
              #
         
     | 
| 
      
 157 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 158 
     | 
    
         
            +
              #
         
     | 
| 
      
 159 
     | 
    
         
            +
              #    map "/events" { run Redisse.redirect_endpoint }
         
     | 
| 
      
 160 
     | 
    
         
            +
              def redirect_endpoint
         
     | 
| 
      
 161 
     | 
    
         
            +
                @redirect_endpoint ||= RedirectEndpoint.new self
         
     | 
| 
      
 162 
     | 
    
         
            +
              end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
              autoload :RedirectEndpoint, __dir__ + '/redisse/redirect_endpoint'
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
            private
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
              def plugins
         
     | 
| 
      
 169 
     | 
    
         
            +
                @plugins ||= []
         
     | 
| 
      
 170 
     | 
    
         
            +
              end
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
              def publisher
         
     | 
| 
      
 173 
     | 
    
         
            +
                @publisher ||= RedisPublisher.new(redis)
         
     | 
| 
      
 174 
     | 
    
         
            +
              end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
              def redis
         
     | 
| 
      
 177 
     | 
    
         
            +
                @redis ||= Redis.new(url: redis_server)
         
     | 
| 
      
 178 
     | 
    
         
            +
              end
         
     | 
| 
      
 179 
     | 
    
         
            +
            end
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            require 'redisse/configuration'
         
     | 
| 
         @@ -0,0 +1,47 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Redisse
         
     | 
| 
      
 2 
     | 
    
         
            +
              extend self
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              # Public: Define the list of channels to subscribe to.
         
     | 
| 
      
 5 
     | 
    
         
            +
              #
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Calls the given block with a Rack environment, the block is expected to
         
     | 
| 
      
 7 
     | 
    
         
            +
              # return a list of channels the current user has access to. The list is then
         
     | 
| 
      
 8 
     | 
    
         
            +
              # coerced using +Kernel#Array+.
         
     | 
| 
      
 9 
     | 
    
         
            +
              #
         
     | 
| 
      
 10 
     | 
    
         
            +
              # Once the block is defined, other calls will be handled by the block
         
     | 
| 
      
 11 
     | 
    
         
            +
              # directly, as if the method had been redefined directly. It simply gives a
         
     | 
| 
      
 12 
     | 
    
         
            +
              # nicer API:
         
     | 
| 
      
 13 
     | 
    
         
            +
              #
         
     | 
| 
      
 14 
     | 
    
         
            +
              #   Redisse.channels do |env|
         
     | 
| 
      
 15 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 16 
     | 
    
         
            +
              #
         
     | 
| 
      
 17 
     | 
    
         
            +
              # vs
         
     | 
| 
      
 18 
     | 
    
         
            +
              #
         
     | 
| 
      
 19 
     | 
    
         
            +
              #   def Redisse.channels(env)
         
     | 
| 
      
 20 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 21 
     | 
    
         
            +
              #
         
     | 
| 
      
 22 
     | 
    
         
            +
              # block - The block that lists the channels for the given Rack environment.
         
     | 
| 
      
 23 
     | 
    
         
            +
              #
         
     | 
| 
      
 24 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 25 
     | 
    
         
            +
              #
         
     | 
| 
      
 26 
     | 
    
         
            +
              #   Redisse.channels do |env|
         
     | 
| 
      
 27 
     | 
    
         
            +
              #     %w( comment post )
         
     | 
| 
      
 28 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 29 
     | 
    
         
            +
              #   # will result in subscriptions to 'comment' and 'post' channels.
         
     | 
| 
      
 30 
     | 
    
         
            +
              #
         
     | 
| 
      
 31 
     | 
    
         
            +
              #   Redisse.channels({})
         
     | 
| 
      
 32 
     | 
    
         
            +
              #   # => ["comment", "post"]
         
     | 
| 
      
 33 
     | 
    
         
            +
              def self.channels(*, &block)
         
     | 
| 
      
 34 
     | 
    
         
            +
                if block
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # overwrite method with block
         
     | 
| 
      
 36 
     | 
    
         
            +
                  define_singleton_method :channels, &block
         
     | 
| 
      
 37 
     | 
    
         
            +
                else
         
     | 
| 
      
 38 
     | 
    
         
            +
                  super
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              self.redis_server = ENV['REDISSE_REDIS'] ||
         
     | 
| 
      
 43 
     | 
    
         
            +
                'redis://localhost:6379/'
         
     | 
| 
      
 44 
     | 
    
         
            +
              self.default_port = ENV['REDISSE_PORT'] ||
         
     | 
| 
      
 45 
     | 
    
         
            +
                8080
         
     | 
| 
      
 46 
     | 
    
         
            +
              self.nginx_internal_url = '/redisse'
         
     | 
| 
      
 47 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,70 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'redisse/server_sent_events'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Redisse
         
     | 
| 
      
 5 
     | 
    
         
            +
              # Internal: Publisher that pushes to Redis with history.
         
     | 
| 
      
 6 
     | 
    
         
            +
              class RedisPublisher
         
     | 
| 
      
 7 
     | 
    
         
            +
                include ServerSentEvents
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                REDISSE_LAST_EVENT_ID = 'redisse:lastEventId'.freeze
         
     | 
| 
      
 10 
     | 
    
         
            +
                HISTORY_SIZE = 100
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def initialize(redis)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @redis = redis or raise 'RedisPublisher needs a Redis client'
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def publish(channel, data, type)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  event_id = @redis.incr(REDISSE_LAST_EVENT_ID)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  event = server_sent_event(data, type: type, id: event_id)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @redis.publish(channel, event)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @redis.zadd(channel, event_id, event)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @redis.zremrangebyrank(channel, 0, -1-HISTORY_SIZE)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  event_id
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              # Internal: Publisher that stores events in memory for easy testing.
         
     | 
| 
      
 27 
     | 
    
         
            +
              #
         
     | 
| 
      
 28 
     | 
    
         
            +
              # See {Redisse#test_mode! Redisse#test_mode!}.
         
     | 
| 
      
 29 
     | 
    
         
            +
              class TestPublisher
         
     | 
| 
      
 30 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @published = []
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                attr_reader :published
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                attr_accessor :filter
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                def publish(channel, data, type)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  return if filter && !(filter === type)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @published << TestEvent.new(channel, data, type)
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              # Define then reopen instead of using the block of Struct.new for YARD.
         
     | 
| 
      
 45 
     | 
    
         
            +
              TestEvent = Struct.new :channel, :data, :type
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              # Public: An event in test mode.
         
     | 
| 
      
 48 
     | 
    
         
            +
              #
         
     | 
| 
      
 49 
     | 
    
         
            +
              # You can re-open or add modules to this class if you want to add behavior
         
     | 
| 
      
 50 
     | 
    
         
            +
              # to events found in {Redisse#published Redisse#published} for easier
         
     | 
| 
      
 51 
     | 
    
         
            +
              # testing.
         
     | 
| 
      
 52 
     | 
    
         
            +
              #
         
     | 
| 
      
 53 
     | 
    
         
            +
              # Examples
         
     | 
| 
      
 54 
     | 
    
         
            +
              #
         
     | 
| 
      
 55 
     | 
    
         
            +
              #   class Redisse::TestEvent
         
     | 
| 
      
 56 
     | 
    
         
            +
              #     def yml
         
     | 
| 
      
 57 
     | 
    
         
            +
              #       YAML.load data
         
     | 
| 
      
 58 
     | 
    
         
            +
              #     end
         
     | 
| 
      
 59 
     | 
    
         
            +
              #
         
     | 
| 
      
 60 
     | 
    
         
            +
              #     def private?
         
     | 
| 
      
 61 
     | 
    
         
            +
              #       channel.start_with? 'private'
         
     | 
| 
      
 62 
     | 
    
         
            +
              #     end
         
     | 
| 
      
 63 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 64 
     | 
    
         
            +
              class TestEvent
         
     | 
| 
      
 65 
     | 
    
         
            +
                # Public: Helper method to parse the Event data as JSON.
         
     | 
| 
      
 66 
     | 
    
         
            +
                def json
         
     | 
| 
      
 67 
     | 
    
         
            +
                  JSON.parse(data)
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'uri'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Redisse
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Public: Rack app that redirects to the Redisse server via X-Accel-Redirect.
         
     | 
| 
      
 6 
     | 
    
         
            +
              class RedirectEndpoint
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(redisse)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @redisse = redisse
         
     | 
| 
      
 10 
     | 
    
         
            +
                  self.base_url = redisse.nginx_internal_url
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def call(env)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  response = Rack::Response.new
         
     | 
| 
      
 15 
     | 
    
         
            +
                  response['X-Accel-Redirect'] = redirect_url(env)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  response
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              private
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def redirect_url(env)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  channels = @redisse.channels(env)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  fail 'Wrong channel "polling"' if channels.include? 'polling'
         
     | 
| 
      
 24 
     | 
    
         
            +
                  fail 'Reserved channel "lastEventId"' if channels.include? 'lastEventId'
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @base_url + '?' + URI.encode_www_form(redirect_options(env) + channels)
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def redirect_options(env)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  params = URI.decode_www_form(env['QUERY_STRING'])
         
     | 
| 
      
 30 
     | 
    
         
            +
                  [].tap do |options|
         
     | 
| 
      
 31 
     | 
    
         
            +
                    options << 'polling'.freeze if params.assoc('polling'.freeze)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    last_event_id_param = params.assoc('lastEventId')
         
     | 
| 
      
 33 
     | 
    
         
            +
                    options << last_event_id_param if last_event_id_param
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def base_url=(url)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  url = String(url)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  url += "/" unless url.end_with? '/'
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @base_url = url
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,205 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'redisse'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'goliath/api'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'rack/accept_media_types'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'goliath/runner'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'em-hiredis'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Redisse
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              # Public: Run the server.
         
     | 
| 
      
 10 
     | 
    
         
            +
              #
         
     | 
| 
      
 11 
     | 
    
         
            +
              # If you use the provided binary you don't need to call this method.
         
     | 
| 
      
 12 
     | 
    
         
            +
              #
         
     | 
| 
      
 13 
     | 
    
         
            +
              # By default, the {#channels} method is called directly.
         
     | 
| 
      
 14 
     | 
    
         
            +
              #
         
     | 
| 
      
 15 
     | 
    
         
            +
              # If {#nginx_internal_url} is set, the channels will actually come from the
         
     | 
| 
      
 16 
     | 
    
         
            +
              # internal redirect URL generated in the Rack app by {#redirect_endpoint}.
         
     | 
| 
      
 17 
     | 
    
         
            +
              def run
         
     | 
| 
      
 18 
     | 
    
         
            +
                run_as_standalone if nginx_internal_url
         
     | 
| 
      
 19 
     | 
    
         
            +
                server = Server.new(self)
         
     | 
| 
      
 20 
     | 
    
         
            +
                runner = Goliath::Runner.new(ARGV, server)
         
     | 
| 
      
 21 
     | 
    
         
            +
                runner.app = Goliath::Rack::Builder.build(self, server)
         
     | 
| 
      
 22 
     | 
    
         
            +
                runner.load_plugins([Server::Stats] + plugins)
         
     | 
| 
      
 23 
     | 
    
         
            +
                runner.run
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            private
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              # Internal: Redefine {#channels} to find channels in the redirect URL.
         
     | 
| 
      
 29 
     | 
    
         
            +
              def run_as_standalone
         
     | 
| 
      
 30 
     | 
    
         
            +
                channels do |env|
         
     | 
| 
      
 31 
     | 
    
         
            +
                  query_string = env['QUERY_STRING'] || ''
         
     | 
| 
      
 32 
     | 
    
         
            +
                  channels = query_string.split('&').map { |channel|
         
     | 
| 
      
 33 
     | 
    
         
            +
                    URI.decode_www_form_component(channel)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  }
         
     | 
| 
      
 35 
     | 
    
         
            +
                  channels.delete('polling')
         
     | 
| 
      
 36 
     | 
    
         
            +
                  channels.delete_if {|channel| channel.start_with?('lastEventId=') }
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
              # Internal: Goliath::API class that defines the server.
         
     | 
| 
      
 41 
     | 
    
         
            +
              #
         
     | 
| 
      
 42 
     | 
    
         
            +
              # See {Redisse#run}.
         
     | 
| 
      
 43 
     | 
    
         
            +
              class Server < Goliath::API
         
     | 
| 
      
 44 
     | 
    
         
            +
                require 'redisse/server/stats'
         
     | 
| 
      
 45 
     | 
    
         
            +
                require 'redisse/server/responses'
         
     | 
| 
      
 46 
     | 
    
         
            +
                include Responses
         
     | 
| 
      
 47 
     | 
    
         
            +
                require 'redisse/server/redis'
         
     | 
| 
      
 48 
     | 
    
         
            +
                include Redis
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                # Public: Delay between receiving a message and closing the connection.
         
     | 
| 
      
 51 
     | 
    
         
            +
                #
         
     | 
| 
      
 52 
     | 
    
         
            +
                # Closing the connection is necessary when using long polling, because the
         
     | 
| 
      
 53 
     | 
    
         
            +
                # client is not able to read the data before the connection is closed. But
         
     | 
| 
      
 54 
     | 
    
         
            +
                # instead of closing immediately, we delay a bit closing the connection to
         
     | 
| 
      
 55 
     | 
    
         
            +
                # give a chance for several messages to be sent in a row.
         
     | 
| 
      
 56 
     | 
    
         
            +
                LONG_POLLING_DELAY = 1
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                # Public: The period between heartbeats in seconds.
         
     | 
| 
      
 59 
     | 
    
         
            +
                HEARTBEAT_PERIOD = 15
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def initialize(redisse)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  @redisse = redisse
         
     | 
| 
      
 63 
     | 
    
         
            +
                  super()
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                def response(env)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  return not_acceptable unless acceptable?(env)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  channels = Array(redisse.channels(env))
         
     | 
| 
      
 69 
     | 
    
         
            +
                  return not_found if channels.empty?
         
     | 
| 
      
 70 
     | 
    
         
            +
                  subscribe(env, channels) or return service_unavailable
         
     | 
| 
      
 71 
     | 
    
         
            +
                  send_history_events(env, channels)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  heartbeat(env)
         
     | 
| 
      
 73 
     | 
    
         
            +
                  streaming_response(200, {
         
     | 
| 
      
 74 
     | 
    
         
            +
                    'Content-Type' => 'text/event-stream',
         
     | 
| 
      
 75 
     | 
    
         
            +
                    'Cache-Control' => 'no-cache',
         
     | 
| 
      
 76 
     | 
    
         
            +
                    'X-Accel-Buffering' => 'no',
         
     | 
| 
      
 77 
     | 
    
         
            +
                  })
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                def on_close(env)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  env.status[:stats][:connected] -= 1
         
     | 
| 
      
 82 
     | 
    
         
            +
                  env.status[:stats][:served]    += 1
         
     | 
| 
      
 83 
     | 
    
         
            +
                  unsubscribe(env)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  stop_heartbeat(env)
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
              private
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                attr_reader :redisse
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def subscribe(env, channels)
         
     | 
| 
      
 92 
     | 
    
         
            +
                  return unless pubsub { env.stream_close }
         
     | 
| 
      
 93 
     | 
    
         
            +
                  env.status[:stats][:connected] += 1
         
     | 
| 
      
 94 
     | 
    
         
            +
                  env.logger.debug { "Subscribing to #{channels}" }
         
     | 
| 
      
 95 
     | 
    
         
            +
                  env_sender = -> event { send_event(env, event) }
         
     | 
| 
      
 96 
     | 
    
         
            +
                  pubsub_subcribe(channels, env_sender)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  env['redisse.unsubscribe'.freeze] = -> do
         
     | 
| 
      
 98 
     | 
    
         
            +
                    pubsub_unsubscribe_proc(channels, env_sender)
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
      
 100 
     | 
    
         
            +
                  true
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                def heartbeat(env)
         
     | 
| 
      
 104 
     | 
    
         
            +
                  env['redisse.heartbeat_timer'.freeze] = EM.add_periodic_timer(HEARTBEAT_PERIOD) do
         
     | 
| 
      
 105 
     | 
    
         
            +
                    env.logger.debug "Sending heartbeat".freeze
         
     | 
| 
      
 106 
     | 
    
         
            +
                    env.stream_send(": hb\n".freeze)
         
     | 
| 
      
 107 
     | 
    
         
            +
                  end
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                def stop_heartbeat(env)
         
     | 
| 
      
 111 
     | 
    
         
            +
                  return unless timer = env['redisse.heartbeat_timer'.freeze]
         
     | 
| 
      
 112 
     | 
    
         
            +
                  env.logger.debug "Stopping heartbeat".freeze
         
     | 
| 
      
 113 
     | 
    
         
            +
                  timer.cancel
         
     | 
| 
      
 114 
     | 
    
         
            +
                end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                def unsubscribe(env)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  return unless unsubscribe = env['redisse.unsubscribe'.freeze]
         
     | 
| 
      
 118 
     | 
    
         
            +
                  env['redisse.unsubscribe'.freeze] = nil
         
     | 
| 
      
 119 
     | 
    
         
            +
                  env.logger.debug "Unsubscribing".freeze
         
     | 
| 
      
 120 
     | 
    
         
            +
                  unsubscribe.call
         
     | 
| 
      
 121 
     | 
    
         
            +
                end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                def send_event(env, event)
         
     | 
| 
      
 124 
     | 
    
         
            +
                  env.status[:stats][:events] += 1
         
     | 
| 
      
 125 
     | 
    
         
            +
                  env.logger.debug { "Sending:\n#{event.chomp.chomp}" }
         
     | 
| 
      
 126 
     | 
    
         
            +
                  env.stream_send(event)
         
     | 
| 
      
 127 
     | 
    
         
            +
                  return unless long_polling?(env)
         
     | 
| 
      
 128 
     | 
    
         
            +
                  env["redisse.long_polling_timer".freeze] ||= EM.add_timer(LONG_POLLING_DELAY) do
         
     | 
| 
      
 129 
     | 
    
         
            +
                    env.stream_close
         
     | 
| 
      
 130 
     | 
    
         
            +
                  end
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                def long_polling?(env)
         
     | 
| 
      
 134 
     | 
    
         
            +
                  key = "redisse.long_polling".freeze
         
     | 
| 
      
 135 
     | 
    
         
            +
                  env.fetch(key) do
         
     | 
| 
      
 136 
     | 
    
         
            +
                    env[key] = Rack::Request.new(env).GET.keys.include?('polling')
         
     | 
| 
      
 137 
     | 
    
         
            +
                  end
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                def send_history_events(env, channels)
         
     | 
| 
      
 141 
     | 
    
         
            +
                  last_event_id = last_event_id(env)
         
     | 
| 
      
 142 
     | 
    
         
            +
                  return unless last_event_id
         
     | 
| 
      
 143 
     | 
    
         
            +
                  EM::Synchrony.next_tick do
         
     | 
| 
      
 144 
     | 
    
         
            +
                    events = events_for_channels(channels, last_event_id)
         
     | 
| 
      
 145 
     | 
    
         
            +
                    env.logger.debug { "Sending #{events.size} history events" }
         
     | 
| 
      
 146 
     | 
    
         
            +
                    if (first = events.first) && first.start_with?('type: missedevents')
         
     | 
| 
      
 147 
     | 
    
         
            +
                      env.status[:stats][:missing] += 1
         
     | 
| 
      
 148 
     | 
    
         
            +
                    end
         
     | 
| 
      
 149 
     | 
    
         
            +
                    events.each { |event| send_event(env, event) }
         
     | 
| 
      
 150 
     | 
    
         
            +
                  end
         
     | 
| 
      
 151 
     | 
    
         
            +
                end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                def last_event_id(env)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  last_event_id = env['HTTP_LAST_EVENT_ID'] ||
         
     | 
| 
      
 155 
     | 
    
         
            +
                    Rack::Request.new(env).GET['lastEventId']
         
     | 
| 
      
 156 
     | 
    
         
            +
                  last_event_id = last_event_id.to_i
         
     | 
| 
      
 157 
     | 
    
         
            +
                  last_event_id.nonzero? && last_event_id
         
     | 
| 
      
 158 
     | 
    
         
            +
                end
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                def events_for_channels(channels, last_event_id)
         
     | 
| 
      
 161 
     | 
    
         
            +
                  events_with_ids = channels.each_with_object([]) { |channel, events|
         
     | 
| 
      
 162 
     | 
    
         
            +
                    channel_events = events_for_channel(channel, last_event_id)
         
     | 
| 
      
 163 
     | 
    
         
            +
                    events.concat(channel_events)
         
     | 
| 
      
 164 
     | 
    
         
            +
                  }.sort_by!(&:last)
         
     | 
| 
      
 165 
     | 
    
         
            +
                  handle_missing_events(events_with_ids, last_event_id)
         
     | 
| 
      
 166 
     | 
    
         
            +
                  events_with_ids.map(&:first)
         
     | 
| 
      
 167 
     | 
    
         
            +
                end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                def handle_missing_events(events_with_ids, last_event_id)
         
     | 
| 
      
 170 
     | 
    
         
            +
                  first_event, first_event_id = events_with_ids.first
         
     | 
| 
      
 171 
     | 
    
         
            +
                  return unless first_event
         
     | 
| 
      
 172 
     | 
    
         
            +
                  if first_event_id == last_event_id
         
     | 
| 
      
 173 
     | 
    
         
            +
                    events_with_ids.shift
         
     | 
| 
      
 174 
     | 
    
         
            +
                  else
         
     | 
| 
      
 175 
     | 
    
         
            +
                    event = ServerSentEvents.server_sent_event(nil, type: :missedevents)
         
     | 
| 
      
 176 
     | 
    
         
            +
                    events_with_ids.unshift([event])
         
     | 
| 
      
 177 
     | 
    
         
            +
                  end
         
     | 
| 
      
 178 
     | 
    
         
            +
                end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                def events_for_channel(channel, last_event_id)
         
     | 
| 
      
 181 
     | 
    
         
            +
                  df = redis.zrangebyscore(channel, last_event_id, '+inf', 'withscores')
         
     | 
| 
      
 182 
     | 
    
         
            +
                  events_scores = EM::Synchrony.sync(df)
         
     | 
| 
      
 183 
     | 
    
         
            +
                  events_scores.each_slice(2).map do |event, score|
         
     | 
| 
      
 184 
     | 
    
         
            +
                    [event, score.to_i]
         
     | 
| 
      
 185 
     | 
    
         
            +
                  end
         
     | 
| 
      
 186 
     | 
    
         
            +
                end
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                def acceptable?(env)
         
     | 
| 
      
 189 
     | 
    
         
            +
                  accept_media_types = Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT'])
         
     | 
| 
      
 190 
     | 
    
         
            +
                  accept_media_types.include?('text/event-stream')
         
     | 
| 
      
 191 
     | 
    
         
            +
                end
         
     | 
| 
      
 192 
     | 
    
         
            +
             
     | 
| 
      
 193 
     | 
    
         
            +
              public
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
                def options_parser(opts, options)
         
     | 
| 
      
 196 
     | 
    
         
            +
                  opts.on '--redis REDIS_URL', 'URL of the Redis connection' do |url|
         
     | 
| 
      
 197 
     | 
    
         
            +
                    redisse.redis_server = url
         
     | 
| 
      
 198 
     | 
    
         
            +
                  end
         
     | 
| 
      
 199 
     | 
    
         
            +
                  default_port = redisse.default_port
         
     | 
| 
      
 200 
     | 
    
         
            +
                  return unless default_port
         
     | 
| 
      
 201 
     | 
    
         
            +
                  options[:port] = default_port
         
     | 
| 
      
 202 
     | 
    
         
            +
                end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
              end
         
     | 
| 
      
 205 
     | 
    
         
            +
            end
         
     |