├── .gitignore ├── LICENSE ├── README.md ├── python ├── chat.py ├── display.py └── requirements.txt └── ruby ├── Gemfile ├── Gemfile.lock ├── cacert.pem ├── chat.rb ├── display.rb └── em-eventsource.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .idea/ 38 | *.iml 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Firebase, https://www.firebase.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Status: Archived 2 | This repository has been archived and is no longer maintained. 3 | 4 | ![status: inactive](https://img.shields.io/badge/status-inactive-red.svg) 5 | 6 | EventSource-Examples 7 | ==================== 8 | 9 | Demonstrates using the EventSource / Server-Sent Events feature of the Firebase REST API 10 | to implement a simple commandline chat client in Ruby and Python. 11 | 12 | Each client has the same architecture: 13 | 14 | * A thread listening the Server-Sent Events endpoint from Firebase 15 | * A thread to POST messages to Firebase 16 | * A thread to manage the UI 17 | * Curses UI. Fancy commandline chat 18 | * Basic UI. Prints messages to stdout 19 | 20 | # Running the Python example 21 | 22 | cd python 23 | 24 | pip install -r requirements.txt 25 | 26 | python chat.py 27 | 28 | # Running the Ruby example 29 | 30 | cd ruby 31 | 32 | bundle install 33 | 34 | ruby chat.rb 35 | 36 | 37 | Use With Your Own Firebase 38 | ========================== 39 | 40 | Simply change the URL constants in chat.py / chat.rb to point to a location in your own Firebase. 41 | 42 | Suggestions / bugfixes / implementations in other languages welcome! 43 | -------------------------------------------------------------------------------- /python/chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from sseclient import SSEClient 4 | from Queue import Queue 5 | import requests 6 | import json 7 | import threading 8 | import display 9 | import socket 10 | import sys 11 | 12 | 13 | #DISPLAY_CLASS = display.BasicDisplay 14 | DISPLAY_CLASS = display.CursesDisplay 15 | 16 | URL = 'https://eventsource.firebaseio-demo.com/.json' 17 | 18 | class ClosableSSEClient(SSEClient): 19 | """ 20 | Hack in some closing functionality on top of the SSEClient 21 | """ 22 | 23 | def __init__(self, *args, **kwargs): 24 | self.should_connect = True 25 | super(ClosableSSEClient, self).__init__(*args, **kwargs) 26 | 27 | def _connect(self): 28 | if self.should_connect: 29 | super(ClosableSSEClient, self)._connect() 30 | else: 31 | raise StopIteration() 32 | 33 | def close(self): 34 | self.should_connect = False 35 | self.retry = 0 36 | # HACK: dig through the sseclient library to the requests library down to the underlying socket. 37 | # then close that to raise an exception to get out of streaming. I should probably file an issue w/ the 38 | # requests library to make this easier 39 | self.resp.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR) 40 | self.resp.raw._fp.fp._sock.close() 41 | 42 | class PostThread(threading.Thread): 43 | 44 | def __init__(self, outbound_queue): 45 | self.outbound_queue = outbound_queue 46 | super(PostThread, self).__init__() 47 | 48 | def run(self): 49 | while True: 50 | msg = self.outbound_queue.get() 51 | if not msg: 52 | break 53 | to_post = json.dumps(msg) 54 | requests.post(URL, data=to_post) 55 | 56 | def close(self): 57 | self.outbound_queue.put(False) 58 | 59 | 60 | class RemoteThread(threading.Thread): 61 | 62 | def __init__(self, message_queue): 63 | self.message_queue = message_queue 64 | super(RemoteThread, self).__init__() 65 | 66 | def run(self): 67 | try: 68 | self.sse = ClosableSSEClient(URL) 69 | for msg in self.sse: 70 | msg_data = json.loads(msg.data) 71 | if msg_data is None: # keep-alives 72 | continue 73 | path = msg_data['path'] 74 | data = msg_data['data'] 75 | if path == '/': 76 | # initial update 77 | if data: 78 | keys = data.keys() 79 | keys.sort() 80 | for k in keys: 81 | self.message_queue.put(data[k]) 82 | else: 83 | # must be a push ID 84 | self.message_queue.put(data) 85 | except socket.error: 86 | pass # this can happen when we close the stream 87 | 88 | def close(self): 89 | if self.sse: 90 | self.sse.close() 91 | 92 | if __name__ == '__main__': 93 | args = sys.argv 94 | client = args[1] if len(args) == 2 else 'python' 95 | outbound_queue = Queue() 96 | inbound_queue = Queue() 97 | post_thread = PostThread(outbound_queue) 98 | post_thread.start() 99 | remote_thread = RemoteThread(inbound_queue) 100 | remote_thread.start() 101 | disp = DISPLAY_CLASS(outbound_queue, client, inbound_queue) 102 | disp.run() 103 | post_thread.join() 104 | remote_thread.close() 105 | remote_thread.join() -------------------------------------------------------------------------------- /python/display.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import sys 3 | import curses 4 | 5 | 6 | class BasicDisplay(): 7 | 8 | class DisplayThread(threading.Thread): 9 | 10 | def __init__(self, inbound_queue): 11 | self.inbound_queue = inbound_queue 12 | super(BasicDisplay.DisplayThread, self).__init__() 13 | 14 | def run(self): 15 | while True: 16 | msg = self.inbound_queue.get() 17 | if not msg: 18 | break 19 | print '%s: %s' % (msg['client'], msg['text']) 20 | 21 | def __init__(self, outbound_queue, client_name, inbound_queue): 22 | self.inbound_queue = inbound_queue 23 | self.client_name = client_name 24 | self.outbound_queue = outbound_queue 25 | self.display_thread = self.setup_display() 26 | self.display_thread.start() 27 | 28 | def setup_display(self): 29 | return self.DisplayThread(self.inbound_queue) 30 | 31 | def cleanup_display(self): 32 | pass # No-op for basic display 33 | 34 | def queue_text(self, text): 35 | packet = {'client': self.client_name, 'text': text} 36 | self.outbound_queue.put(packet) 37 | 38 | def run(self): 39 | try: 40 | map(self.queue_text, self.input_lines()) 41 | finally: 42 | self.cleanup_display() 43 | self.close() 44 | 45 | def input_lines(self): 46 | while not sys.stdin.closed: 47 | line = sys.stdin.readline().strip() 48 | if line == '': 49 | raise StopIteration() 50 | yield line 51 | 52 | def close(self): 53 | self.inbound_queue.put(False) 54 | self.outbound_queue.put(False) 55 | self.display_thread.join() 56 | 57 | 58 | class CursesDisplay(BasicDisplay): 59 | KEY_ENTER = 10 60 | 61 | class DisplayThread(threading.Thread): 62 | 63 | def __init__(self, inbound_queue, chat_win, limit): 64 | self.inbound_queue = inbound_queue 65 | self.chat_win = chat_win 66 | self.limit = limit 67 | super(CursesDisplay.DisplayThread, self).__init__() 68 | 69 | def run(self): 70 | messages = [] 71 | while True: 72 | msg = self.inbound_queue.get() 73 | if not msg: 74 | break 75 | messages.append(msg) 76 | # HACK! SUPER INEFFICIENT! 77 | messages = messages[-self.limit:] 78 | self.chat_win.erase() 79 | for i, message in enumerate(messages): 80 | self.chat_win.addstr(i, 0, '%s: %s' % (message['client'], message['text'])) 81 | self.chat_win.refresh() 82 | 83 | def setup_display(self): 84 | limit = self.setup_screen() 85 | return self.DisplayThread(self.inbound_queue, self.chat_win, limit) 86 | 87 | def setup_screen(self): 88 | # setup curses / screen config 89 | self.scr = curses.initscr() 90 | self.scr.keypad(1) 91 | curses.cbreak() 92 | curses.curs_set(False) 93 | rows, cols = self.scr.getmaxyx() 94 | 95 | # create our windows 96 | self.div_win = curses.newwin(1, cols + 1, rows - 3, 0) 97 | self.chat_win = curses.newwin(rows - 3, cols, 0, 0) 98 | self.msg_win = curses.newwin(1, cols, rows - 2, 0) 99 | self.instructions_win = curses.newwin(1, cols, rows - 1, 0) 100 | 101 | # set up the divider 102 | div = '=' * cols 103 | self.div_win.addstr(0, 0, div) 104 | self.div_win.refresh() 105 | 106 | self.instructions_win.addstr(0, 0, 'Type a message, Hit to send. An empty message quits the program') 107 | self.instructions_win.refresh() 108 | 109 | return rows - 4 110 | 111 | def input_loop(self): 112 | current_msg = '' 113 | while True: 114 | c = self.msg_win.getch() 115 | if c == CursesDisplay.KEY_ENTER: 116 | if current_msg == '': 117 | break 118 | else: 119 | self.queue_text(current_msg) 120 | self.msg_win.erase() 121 | self.msg_win.refresh() 122 | current_msg = '' 123 | else: 124 | s = chr(c) 125 | current_msg += s 126 | 127 | def input_lines(self): 128 | current_msg = '' 129 | while True: 130 | c = self.msg_win.getch() 131 | if c == CursesDisplay.KEY_ENTER: 132 | if current_msg == '': 133 | raise StopIteration 134 | else: 135 | self.msg_win.erase() 136 | self.msg_win.refresh() 137 | line = current_msg 138 | current_msg = '' 139 | yield line 140 | else: 141 | s = chr(c) 142 | current_msg += s 143 | 144 | def cleanup_display(self): 145 | self.scr.keypad(0) 146 | curses.nocbreak() 147 | curses.curs_set(True) 148 | curses.endwin() -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.0.1 2 | sseclient==0.0.6 3 | wsgiref==0.1.2 4 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "eventmachine", "~> 1.0.8" 4 | gem "em-http-request", "~> 1.1.1" 5 | gem "httparty", "~> 0.12.0" 6 | gem "curses", "~> 1.0.1" 7 | -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.5) 5 | cookiejar (0.3.0) 6 | curses (1.0.1) 7 | em-http-request (1.1.1) 8 | addressable (>= 2.3.4) 9 | cookiejar 10 | em-socksify (>= 0.3) 11 | eventmachine (>= 1.0.3) 12 | http_parser.rb (>= 0.6.0.beta.2) 13 | em-socksify (0.3.0) 14 | eventmachine (>= 1.0.0.beta.4) 15 | eventmachine (1.0.8) 16 | http_parser.rb (0.6.0.beta.2) 17 | httparty (0.12.0) 18 | json (~> 1.8) 19 | multi_xml (>= 0.5.2) 20 | json (1.8.1) 21 | multi_xml (0.5.5) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | curses (~> 1.0.1) 28 | em-http-request (~> 1.1.1) 29 | eventmachine (~> 1.0.8) 30 | httparty (~> 0.12.0) 31 | 32 | BUNDLED WITH 33 | 1.10.6 34 | -------------------------------------------------------------------------------- /ruby/chat.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require './em-eventsource' 4 | require 'thread' 5 | require 'httparty' 6 | require 'json' 7 | require './display' 8 | 9 | URL = 'https://eventsource.firebaseio-demo.com/.json' 10 | 11 | class PostThread 12 | include HTTParty 13 | 14 | ssl_ca_file 'cacert.pem' 15 | 16 | def initialize(outbound_queue) 17 | @outbound_queue = outbound_queue 18 | @post_uri = URL 19 | end 20 | 21 | def run 22 | @thread = Thread.new do 23 | while true 24 | msg = @outbound_queue.pop 25 | if msg 26 | send_msg(msg) 27 | else 28 | break 29 | end 30 | end 31 | end 32 | end 33 | 34 | def join 35 | @thread.join 36 | end 37 | 38 | private 39 | 40 | def send_msg(msg) 41 | self.class.post(@post_uri, {:body => msg.to_json, :headers => { 'Content-Type' => 'application/json' }}) 42 | end 43 | 44 | end 45 | 46 | class RemoteThread 47 | 48 | def initialize(message_queue) 49 | @message_queue = message_queue 50 | end 51 | 52 | def run 53 | @thread = Thread.new do 54 | EM.run do 55 | @source = EventMachine::EventSource.new(URL, headers = {"Accept" => "text/event-stream"}) 56 | #@source.message do |message| 57 | # puts "new message #{message}" 58 | #end 59 | @source.on "keep-alive" do |unused| 60 | # just a keep-alive, do nothing. unused message is null 61 | end 62 | @source.on "put" do |put| 63 | msg_data = JSON.parse(put) 64 | path = msg_data['path'] 65 | data = msg_data['data'] 66 | if path == "/" 67 | if data 68 | keys = data.keys 69 | keys.sort! 70 | keys.each do |key| 71 | @message_queue << data[key] 72 | end 73 | end 74 | else 75 | # Must be a Push ID 76 | @message_queue << data 77 | end 78 | end 79 | @source.on "patch" do |merge| 80 | msg_data = JSON.parse(merge) 81 | path = msg_data['path'] 82 | data = msg_data['data'] 83 | if path == "/" 84 | if data 85 | keys = data.keys 86 | keys.sort! 87 | keys.each do |key| 88 | @message_queue << data[key] 89 | end 90 | end 91 | else 92 | @message_queue << data 93 | end 94 | end 95 | @source.error do |error| 96 | puts "error: #{error}" 97 | @source.close 98 | end 99 | @source.open do 100 | #puts "opened" 101 | end 102 | @source.start 103 | end 104 | end 105 | end 106 | 107 | def close 108 | @source.close 109 | EM.schedule { 110 | EM.stop 111 | } 112 | end 113 | 114 | def join 115 | @thread.join 116 | end 117 | end 118 | 119 | 120 | client = if ARGV.length == 1 then 121 | ARGV[0] 122 | else 123 | "ruby" 124 | end 125 | 126 | outbound_queue = Queue.new 127 | inbound_queue = Queue.new 128 | 129 | post_thread = PostThread.new(outbound_queue) 130 | post_thread.run 131 | 132 | remote_thread = RemoteThread.new(inbound_queue) 133 | remote_thread.run 134 | 135 | #disp = BasicDisplay.new(outbound_queue, client, inbound_queue) 136 | disp = CursesDisplay.new(outbound_queue, client, inbound_queue) 137 | disp.run 138 | 139 | post_thread.join 140 | 141 | remote_thread.close 142 | remote_thread.join 143 | -------------------------------------------------------------------------------- /ruby/display.rb: -------------------------------------------------------------------------------- 1 | class BasicDisplay 2 | 3 | class DisplayThread 4 | 5 | def initialize(inbound_queue) 6 | @inbound_queue = inbound_queue 7 | end 8 | 9 | def run 10 | @thread = Thread.new do 11 | while true 12 | msg = @inbound_queue.pop 13 | if msg 14 | puts "#{msg['client']}: #{msg['text']}" 15 | else 16 | break 17 | end 18 | end 19 | end 20 | end 21 | 22 | def join 23 | @thread.join 24 | end 25 | end 26 | 27 | def initialize(outbound_queue, client_name, inbound_queue) 28 | @outbound_queue = outbound_queue 29 | @client_name = client_name 30 | @inbound_queue = inbound_queue 31 | @display_thread = setup_display(inbound_queue) 32 | @display_thread.run 33 | end 34 | 35 | def setup_display(inbound_queue) 36 | DisplayThread.new(inbound_queue) 37 | end 38 | 39 | def run 40 | input_lines.each { |line| queue_text(line) } 41 | 42 | cleanup_display 43 | close 44 | end 45 | 46 | def input_lines 47 | Enumerator.new do |y| 48 | while line = STDIN.gets 49 | line.strip! 50 | if line.empty? 51 | break 52 | else 53 | y << line 54 | end 55 | end 56 | end 57 | end 58 | 59 | def cleanup_display 60 | # No-op 61 | end 62 | 63 | def queue_text(msg) 64 | packet = {:client => @client_name, :text => msg} 65 | @outbound_queue << packet 66 | end 67 | 68 | def close 69 | @outbound_queue << false 70 | @inbound_queue << false 71 | @display_thread.join 72 | end 73 | end 74 | 75 | class CursesDisplay < BasicDisplay 76 | require 'curses' 77 | 78 | class CursesDisplayThread 79 | 80 | def initialize(inbound_queue, chat_win, limit) 81 | @inbound_queue = inbound_queue 82 | @chat_win = chat_win 83 | @limit = limit 84 | end 85 | 86 | def run 87 | @thread = Thread.new do 88 | messages = [] 89 | while true 90 | msg = @inbound_queue.pop 91 | if msg 92 | messages << msg 93 | min = [messages.length, @limit].min 94 | messages = messages[-min, min] 95 | @chat_win.clear 96 | messages.each_with_index do |value, index| 97 | @chat_win.setpos(index, 0) 98 | @chat_win.addstr("#{value["client"]}: #{value["text"]}") 99 | end 100 | @chat_win.refresh 101 | else 102 | break 103 | end 104 | end 105 | end 106 | end 107 | 108 | def join 109 | @thread.join 110 | end 111 | end 112 | 113 | def setup_display(inbound_queue) 114 | limit = setup_screen 115 | CursesDisplayThread.new(inbound_queue, @chat_win, limit) 116 | end 117 | 118 | def setup_screen 119 | @scr = Curses.init_screen 120 | @scr.keypad(true) 121 | Curses.cbreak 122 | Curses.curs_set(0) 123 | rows = @scr.maxy 124 | cols = @scr.maxx 125 | 126 | #create our windows 127 | @div_win = Curses::Window.new(1, cols + 1, rows - 3, 0) 128 | @chat_win = Curses::Window.new(rows - 3, cols, 0, 0) 129 | @msg_win = Curses::Window.new(1, cols, rows - 2, 0) 130 | @instructions_win = Curses::Window.new(1, cols, rows - 1, 0) 131 | 132 | # setup the divider 133 | div = "=" * cols 134 | @div_win.addstr(div) 135 | @div_win.refresh 136 | 137 | @instructions_win.addstr("Type a message, Hit to send. An empty message quits the program") 138 | @instructions_win.refresh 139 | 140 | rows - 4 141 | end 142 | 143 | def input_lines 144 | Enumerator.new do |y| 145 | current_msg = '' 146 | while true 147 | c = @msg_win.getch 148 | if c == 10 149 | if current_msg.empty? 150 | break 151 | else 152 | @msg_win.clear 153 | @msg_win.refresh 154 | line = current_msg 155 | current_msg = '' 156 | y << line 157 | end 158 | else 159 | current_msg += c.chr 160 | end 161 | end 162 | end 163 | end 164 | 165 | def cleanup_display 166 | @scr.keypad(false) 167 | Curses.nocbreak 168 | Curses.curs_set(1) 169 | Curses.close_screen 170 | end 171 | 172 | end -------------------------------------------------------------------------------- /ruby/em-eventsource.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Adapted from original source at https://github.com/AF83/em-eventsource 4 | 5 | require "eventmachine" 6 | require "em-http-request" 7 | 8 | module EventMachine 9 | # EventSource 10 | # dev.w3.org/html5/eventsource/ 11 | class EventSource 12 | # Get API url 13 | attr_reader :url 14 | # Get ready state 15 | attr_reader :ready_state 16 | # Get current retry value (in seconds) 17 | attr_reader :retry 18 | # Override retry value (in seconds) 19 | attr_writer :retry 20 | # Get value of last event id 21 | attr_reader :last_event_id 22 | # Get the inactivity timeout 23 | attr_reader :inactivity_timeout 24 | # Set the inactivity timeout 25 | attr_writer :inactivity_timeout 26 | # Ready state 27 | # The connection has not yet been established, or it was closed and the user agent is reconnecting. 28 | CONNECTING = 0 29 | # The user agent has an open connection and is dispatching events as it receives them. 30 | OPEN = 1 31 | # The connection is not open, and the user agent is not trying to reconnect. Either there was a fatal error or the close() method was invoked. 32 | CLOSED = 2 33 | 34 | # Create a new stream 35 | # 36 | # url - the url as string 37 | # query - the query string as hash 38 | # headers - the headers for the request as hash 39 | def initialize(url, query={}, headers={}) 40 | @url = url 41 | @query = query 42 | @headers = headers 43 | @ready_state = CLOSED 44 | 45 | @last_event_id = nil 46 | @retry = 3 # seconds 47 | @inactivity_timeout = 60 # seconds 48 | 49 | @opens = [] 50 | @errors = [] 51 | @messages = [] 52 | @on = {} 53 | @middlewares = [] 54 | end 55 | 56 | # Add open event handler 57 | # 58 | # Returns nothing 59 | def open(&block) 60 | @opens << block 61 | end 62 | 63 | # Add a specific event handler 64 | # 65 | # name - name of event 66 | # 67 | # Returns nothing 68 | def on(name, &block) 69 | @on[name] ||= [] 70 | @on[name] << block 71 | end 72 | 73 | # Add message event handler 74 | # 75 | # Returns nothing 76 | def message(&block) 77 | @messages << block 78 | end 79 | 80 | # Add error event handler 81 | # 82 | # Returns nothing 83 | def error(&block) 84 | @errors << block 85 | end 86 | 87 | # Add a middleware 88 | # 89 | # *args - the middleware class 90 | # 91 | # Returns nothing 92 | def use(*args, &block) 93 | @middlewares << (args << block) 94 | end 95 | 96 | # Start subscription 97 | # 98 | # Returns nothing 99 | def start 100 | @ready_state = CONNECTING 101 | listen 102 | end 103 | 104 | # Cancel subscription 105 | # 106 | # Returns nothing 107 | def close 108 | @ready_state = CLOSED 109 | @conn.close('requested') if @conn 110 | end 111 | 112 | protected 113 | 114 | def listen 115 | @conn, @req = prepare_request 116 | @req.headers(&method(:handle_headers)) 117 | @req.errback(&method(:handle_reconnect)) 118 | #@req.callback(&method(:handle_reconnect)) 119 | buffer = "" 120 | @req.stream do |chunk| 121 | buffer += chunk 122 | # TODO: manage \r, \r\n, \n 123 | while index = buffer.index("\n\n") 124 | stream = buffer.slice!(0..index) 125 | handle_stream(stream) 126 | end 127 | end 128 | end 129 | 130 | def handle_reconnect(*args) 131 | return if @ready_state == CLOSED 132 | @ready_state = CONNECTING 133 | @errors.each { |error| error.call("Connection lost. Reconnecting.") } 134 | EM.add_timer(@retry) do 135 | listen 136 | end 137 | end 138 | 139 | def handle_headers(headers) 140 | if headers.status == 307 141 | new_url = headers['LOCATION'] 142 | close 143 | @url = new_url 144 | start 145 | return 146 | elsif headers.status != 200 147 | close 148 | @errors.each { |error| error.call("Unexpected response status #{headers.status}") } 149 | return 150 | end 151 | if /^text\/event-stream/.match headers['CONTENT_TYPE'] 152 | @ready_state = OPEN 153 | @opens.each { |open| open.call } 154 | else 155 | close 156 | @errors.each { |error| error.call("The content-type '#{headers['CONTENT_TYPE']}' is not text/event-stream") } 157 | end 158 | end 159 | 160 | def handle_stream(stream) 161 | data = "" 162 | name = nil 163 | stream.split("\n").each do |part| 164 | /^data:(.+)$/.match(part) do |m| 165 | data += m[1].strip 166 | data += "\n" 167 | end 168 | /^id:(.+)$/.match(part) do |m| 169 | @last_event_id = m[1].strip 170 | end 171 | /^event:(.+)$/.match(part) do |m| 172 | name = m[1].strip 173 | end 174 | /^retry:(.+)$/.match(part) do |m| 175 | if m[1].strip! =~ /^[0-9]+$/ 176 | @retry = m[1].to_i 177 | end 178 | end 179 | end 180 | return if data.empty? 181 | data.chomp!("\n") 182 | if name.nil? 183 | @messages.each { |message| message.call(data) } 184 | else 185 | @on[name].each { |message| message.call(data) } if not @on[name].nil? 186 | end 187 | end 188 | 189 | def prepare_request 190 | conn = EM::HttpRequest.new(@url, :inactivity_timeout => @inactivity_timeout) 191 | @middlewares.each { |middleware| 192 | block = middleware.pop 193 | conn.use *middleware, &block 194 | } 195 | headers = @headers.merge({'Cache-Control' => 'no-cache', 'Accept' => 'text/event-stream'}) 196 | headers.merge!({'Last-Event-Id' => @last_event_id }) if not @last_event_id.nil? 197 | [conn, conn.get({ :query => @query, 198 | :head => headers})] 199 | end 200 | end 201 | end 202 | --------------------------------------------------------------------------------