pakyow-realtime 0.11.3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{pakyow-realtime/CHANGELOG.md → CHANGELOG.md} +5 -0
- data/LICENSE +4 -0
- data/{pakyow-realtime/README.md → README.md} +1 -2
- data/lib/pakyow/environment/realtime/config.rb +29 -0
- data/lib/pakyow/realtime/actions/upgrader.rb +29 -0
- data/lib/pakyow/realtime/behavior/config.rb +42 -0
- data/lib/pakyow/realtime/behavior/rendering/install_websocket.rb +57 -0
- data/lib/pakyow/realtime/behavior/serialization.rb +42 -0
- data/lib/pakyow/realtime/behavior/server.rb +42 -0
- data/lib/pakyow/realtime/behavior/silencing.rb +25 -0
- data/lib/pakyow/realtime/channel.rb +23 -0
- data/lib/pakyow/realtime/context.rb +38 -0
- data/lib/pakyow/realtime/framework.rb +49 -0
- data/lib/pakyow/realtime/helpers/broadcasting.rb +13 -0
- data/lib/pakyow/realtime/helpers/socket.rb +13 -0
- data/lib/pakyow/realtime/helpers/subscriptions.rb +35 -0
- data/lib/pakyow/realtime/server/adapters/memory.rb +127 -0
- data/lib/pakyow/realtime/server/adapters/redis.rb +277 -0
- data/lib/pakyow/realtime/server.rb +152 -0
- data/lib/pakyow/realtime/websocket.rb +157 -0
- data/lib/pakyow/realtime.rb +13 -0
- metadata +73 -44
- data/pakyow-realtime/LICENSE +0 -20
- data/pakyow-realtime/lib/pakyow/realtime/config.rb +0 -20
- data/pakyow-realtime/lib/pakyow/realtime/connection.rb +0 -18
- data/pakyow-realtime/lib/pakyow/realtime/context.rb +0 -68
- data/pakyow-realtime/lib/pakyow/realtime/delegate.rb +0 -112
- data/pakyow-realtime/lib/pakyow/realtime/exceptions.rb +0 -6
- data/pakyow-realtime/lib/pakyow/realtime/ext/request.rb +0 -10
- data/pakyow-realtime/lib/pakyow/realtime/helpers.rb +0 -40
- data/pakyow-realtime/lib/pakyow/realtime/hooks.rb +0 -41
- data/pakyow-realtime/lib/pakyow/realtime/message_handler.rb +0 -57
- data/pakyow-realtime/lib/pakyow/realtime/message_handlers/call_route.rb +0 -34
- data/pakyow-realtime/lib/pakyow/realtime/message_handlers/ping.rb +0 -8
- data/pakyow-realtime/lib/pakyow/realtime/redis_subscription.rb +0 -61
- data/pakyow-realtime/lib/pakyow/realtime/registries/redis_registry.rb +0 -107
- data/pakyow-realtime/lib/pakyow/realtime/registries/simple_registry.rb +0 -40
- data/pakyow-realtime/lib/pakyow/realtime/websocket.rb +0 -209
- data/pakyow-realtime/lib/pakyow/realtime.rb +0 -19
- data/pakyow-realtime/lib/pakyow-realtime.rb +0 -1
@@ -1,61 +0,0 @@
|
|
1
|
-
require 'redis'
|
2
|
-
require 'concurrent'
|
3
|
-
|
4
|
-
module Pakyow
|
5
|
-
module Realtime
|
6
|
-
# Manages channel subscriptions for this application instance's WebSockets.
|
7
|
-
#
|
8
|
-
# @api private
|
9
|
-
class RedisSubscription
|
10
|
-
SIGNAL_UNSUBSCRIBE = 'unsubscribe'
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
@redis = ::Redis.new(Config.realtime.redis)
|
14
|
-
end
|
15
|
-
|
16
|
-
def subscribe(channels = [])
|
17
|
-
channels << signal_channel
|
18
|
-
|
19
|
-
Concurrent::Future.execute {
|
20
|
-
@redis.subscribe(*channels) do |on|
|
21
|
-
on.message do |channel, msg|
|
22
|
-
begin
|
23
|
-
msg = JSON.parse(msg)
|
24
|
-
|
25
|
-
if channel == signal_channel
|
26
|
-
if msg['signal'] == SIGNAL_UNSUBSCRIBE
|
27
|
-
@resubscribe_channels = msg['resubscribe_channels']
|
28
|
-
@redis.unsubscribe
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
if msg.is_a?(Hash)
|
33
|
-
msg[:__propagated] = true
|
34
|
-
elsif msg.is_a?(Array)
|
35
|
-
msg << :__propagated
|
36
|
-
end
|
37
|
-
|
38
|
-
context = Pakyow::Realtime::Context.new(Pakyow.app)
|
39
|
-
|
40
|
-
if msg.key?('key')
|
41
|
-
context.push_message_to_socket_with_key(msg['message'], msg['channel'], msg['key'], true)
|
42
|
-
else
|
43
|
-
context.push(msg, channel)
|
44
|
-
end
|
45
|
-
rescue StandardError => e
|
46
|
-
Pakyow.logger.error "RedisSubscription encountered a fatal error:"
|
47
|
-
Pakyow.logger.error e.message
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
subscribe(@resubscribe_channels) if @resubscribe_channels
|
53
|
-
}
|
54
|
-
end
|
55
|
-
|
56
|
-
def signal_channel
|
57
|
-
"pw:#{object_id}:signal"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
@@ -1,107 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'redis'
|
3
|
-
require 'singleton'
|
4
|
-
|
5
|
-
require 'pakyow/realtime/redis_subscription'
|
6
|
-
require 'pakyow/realtime/registries/simple_registry'
|
7
|
-
|
8
|
-
module Pakyow
|
9
|
-
module Realtime
|
10
|
-
def self.redis
|
11
|
-
$redis ||= Redis.new(Config.realtime.redis)
|
12
|
-
end
|
13
|
-
# Manages WebSocket connections and their subscriptions in Redis.
|
14
|
-
#
|
15
|
-
# This is the default registry in production systems and is required in
|
16
|
-
# deployments with more than one app instance.
|
17
|
-
#
|
18
|
-
# @api private
|
19
|
-
class RedisRegistry < SimpleRegistry
|
20
|
-
attr_reader :subscriber
|
21
|
-
|
22
|
-
def channels_for_key(key)
|
23
|
-
value = Pakyow::Realtime.redis.hget(channel_key, key)
|
24
|
-
value ? JSON.parse(value) : []
|
25
|
-
end
|
26
|
-
|
27
|
-
def unregister_key(key)
|
28
|
-
super
|
29
|
-
|
30
|
-
Pakyow::Realtime.redis.hdel(channel_key, key)
|
31
|
-
resubscribe
|
32
|
-
end
|
33
|
-
|
34
|
-
def subscribe_to_channels_for_key(channels, key)
|
35
|
-
channels << "pw:socket:#{key}"
|
36
|
-
super
|
37
|
-
|
38
|
-
resubscribe
|
39
|
-
|
40
|
-
new_channels = channels_for_key(key).concat(Array.ensure(channels)).uniq
|
41
|
-
Pakyow::Realtime.redis.hset(channel_key, key, new_channels.to_json)
|
42
|
-
end
|
43
|
-
|
44
|
-
def unsubscribe_to_channels_for_key(channels, key)
|
45
|
-
super
|
46
|
-
|
47
|
-
resubscribe
|
48
|
-
|
49
|
-
new_channels = channels_for_key(key) - Array.ensure(channels)
|
50
|
-
Pakyow::Realtime.redis.hset(channel_key, key, new_channels.to_json)
|
51
|
-
end
|
52
|
-
|
53
|
-
def propagates?
|
54
|
-
true
|
55
|
-
end
|
56
|
-
|
57
|
-
def propagate(message, channels)
|
58
|
-
message_json = message.to_json
|
59
|
-
|
60
|
-
channels.each do |channel|
|
61
|
-
Pakyow::Realtime.redis.publish(channel, message_json)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def subscribe_for_propagation(channels, key)
|
66
|
-
@channels[key] ||= []
|
67
|
-
@channels[key].concat(Array.ensure(channels)) << "pw:socket:#{key}"
|
68
|
-
@channels[key].uniq!
|
69
|
-
|
70
|
-
resubscribe
|
71
|
-
end
|
72
|
-
|
73
|
-
def push_message_to_socket_with_key(message, channel, key)
|
74
|
-
propagate({
|
75
|
-
key: key,
|
76
|
-
channel: channel,
|
77
|
-
message: message
|
78
|
-
},
|
79
|
-
|
80
|
-
["pw:socket:#{key}"])
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
|
85
|
-
# Tells the subscription to unsubscribe from the current
|
86
|
-
# list of channels, then subscribes it to the new list.
|
87
|
-
def resubscribe
|
88
|
-
channels = @channels.values.flatten.uniq
|
89
|
-
|
90
|
-
if @subscriber
|
91
|
-
Pakyow::Realtime.redis.publish(@subscriber.signal_channel, {
|
92
|
-
'signal' => RedisSubscription::SIGNAL_UNSUBSCRIBE,
|
93
|
-
'resubscribe_channels' => channels
|
94
|
-
}.to_json)
|
95
|
-
else
|
96
|
-
@subscriber = RedisSubscription.new
|
97
|
-
@subscriber.subscribe(channels)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# Returns the key used to store channels.
|
102
|
-
def channel_key
|
103
|
-
Config.realtime.redis_key
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
require 'singleton'
|
2
|
-
|
3
|
-
module Pakyow
|
4
|
-
module Realtime
|
5
|
-
# Manages WebSocket connections and their subscriptions in memory.
|
6
|
-
#
|
7
|
-
# Intended only for use in development or single app-instance deployments.
|
8
|
-
#
|
9
|
-
# @api private
|
10
|
-
class SimpleRegistry
|
11
|
-
include Singleton
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
@channels = {}
|
15
|
-
end
|
16
|
-
|
17
|
-
def channels_for_key(key)
|
18
|
-
@channels.fetch(key, [])
|
19
|
-
end
|
20
|
-
|
21
|
-
def unregister_key(key)
|
22
|
-
@channels.delete(key)
|
23
|
-
end
|
24
|
-
|
25
|
-
def subscribe_to_channels_for_key(channels, key)
|
26
|
-
@channels[key] ||= []
|
27
|
-
@channels[key].concat(Array.ensure(channels)).uniq!
|
28
|
-
end
|
29
|
-
|
30
|
-
def unsubscribe_to_channels_for_key(channels, key)
|
31
|
-
@channels[key] ||= []
|
32
|
-
@channels[key] = @channels[key] - Array.ensure(channels)
|
33
|
-
end
|
34
|
-
|
35
|
-
def propagates?
|
36
|
-
false
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,209 +0,0 @@
|
|
1
|
-
require 'concurrent'
|
2
|
-
require 'websocket'
|
3
|
-
|
4
|
-
require_relative 'connection'
|
5
|
-
|
6
|
-
module Pakyow
|
7
|
-
module Realtime
|
8
|
-
# Hijacks a request, performs the handshake, and creates an async object
|
9
|
-
# for handling incoming and outgoing messages in an asynchronous manner.
|
10
|
-
#
|
11
|
-
# @api private
|
12
|
-
class Websocket < Connection
|
13
|
-
attr_reader :parser, :socket, :key
|
14
|
-
|
15
|
-
@event_handlers = {}
|
16
|
-
|
17
|
-
def initialize(req, key)
|
18
|
-
@req = req
|
19
|
-
@key = key
|
20
|
-
|
21
|
-
@socket = hijack(req)
|
22
|
-
@server = handshake(req)
|
23
|
-
setup
|
24
|
-
end
|
25
|
-
|
26
|
-
def registered
|
27
|
-
handle_ws_join
|
28
|
-
end
|
29
|
-
|
30
|
-
def shutdown
|
31
|
-
delegate.unregister(@key)
|
32
|
-
self.class.handle_event(:leave, @req)
|
33
|
-
|
34
|
-
@socket.close if @socket && !@socket.closed?
|
35
|
-
@shutdown = true
|
36
|
-
|
37
|
-
@reader = nil
|
38
|
-
end
|
39
|
-
|
40
|
-
def shutdown?
|
41
|
-
@shutdown == true
|
42
|
-
end
|
43
|
-
|
44
|
-
def push(msg)
|
45
|
-
json = JSON.pretty_generate(msg)
|
46
|
-
logger.debug "(ws.#{@key}) sending message: #{json}\n"
|
47
|
-
|
48
|
-
frame = WebSocket::Frame::Outgoing::Server.new(
|
49
|
-
version: @server.version,
|
50
|
-
data: json,
|
51
|
-
type: :text)
|
52
|
-
@socket.write(frame.to_s)
|
53
|
-
rescue StandardError => e
|
54
|
-
logger.error "(#{@key}): WebSocket encountered a fatal error:"
|
55
|
-
logger.error e.message
|
56
|
-
|
57
|
-
# something went wrong (like a broken pipe); shutdown
|
58
|
-
# and let the socket reconnect if it's still around
|
59
|
-
shutdown
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.on(event, &block)
|
63
|
-
(@event_handlers[event.to_sym] ||= []) << block
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
def hijack(req)
|
69
|
-
if req.env['rack.hijack']
|
70
|
-
req.env['rack.hijack'].call
|
71
|
-
return req.env['rack.hijack_io']
|
72
|
-
else
|
73
|
-
logger.info "there's no socket to hijack :("
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def handshake(req)
|
78
|
-
data = "#{req.env['REQUEST_METHOD']} #{req.env['REQUEST_URI']} #{req.env['SERVER_PROTOCOL']}\r\n"
|
79
|
-
req.env.each_pair do |key, val|
|
80
|
-
if key =~ /^HTTP_(.*)/
|
81
|
-
name = rack_env_key_to_http_header_name($1)
|
82
|
-
data << "#{name}: #{val}\r\n"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
data << "\r\n"
|
86
|
-
|
87
|
-
server = WebSocket::Handshake::Server.new
|
88
|
-
server << data
|
89
|
-
|
90
|
-
fail HandshakeError, "(ws.#{@key}) error during handshake" unless server.valid?
|
91
|
-
@socket.write(server.to_s)
|
92
|
-
|
93
|
-
server
|
94
|
-
end
|
95
|
-
|
96
|
-
def rack_env_key_to_http_header_name(key)
|
97
|
-
name = key.downcase.gsub('_', '-')
|
98
|
-
name[0] = name[0].upcase
|
99
|
-
name.gsub!(/-(.)/) do |chr|
|
100
|
-
chr.upcase
|
101
|
-
end
|
102
|
-
name
|
103
|
-
end
|
104
|
-
|
105
|
-
def setup
|
106
|
-
logger.info "(ws.#{@key}) client established connection"
|
107
|
-
|
108
|
-
@parser = Parser.new(version: @server.version)
|
109
|
-
|
110
|
-
@parser.on :message do |message|
|
111
|
-
handle_ws_message(message)
|
112
|
-
end
|
113
|
-
|
114
|
-
@parser.on :error do |error|
|
115
|
-
logger.error "(ws.#{@key}) encountered error #{error}"
|
116
|
-
handle_ws_error(error)
|
117
|
-
end
|
118
|
-
|
119
|
-
@parser.on :close do |status, message|
|
120
|
-
logger.info "(ws.#{@key}) client closed connection"
|
121
|
-
handle_ws_close(status, message)
|
122
|
-
end
|
123
|
-
|
124
|
-
Concurrent::Future.execute {
|
125
|
-
begin
|
126
|
-
loop do
|
127
|
-
break if shutdown?
|
128
|
-
@parser << @socket.read_nonblock(1024)
|
129
|
-
end
|
130
|
-
rescue ::IO::WaitReadable
|
131
|
-
IO.select([@socket])
|
132
|
-
retry
|
133
|
-
end
|
134
|
-
}
|
135
|
-
end
|
136
|
-
|
137
|
-
def handle_ws_message(message)
|
138
|
-
parsed = JSON.parse(message)
|
139
|
-
logger.debug "(ws.#{@key}) received message: #{JSON.pretty_generate(parsed)}\n"
|
140
|
-
push(MessageHandler.handle(parsed, @req.env['rack.session']))
|
141
|
-
rescue StandardError => e
|
142
|
-
logger.error "(#{@key}): WebSocket encountered an error:"
|
143
|
-
logger.error e.message
|
144
|
-
|
145
|
-
e.backtrace.each do |line|
|
146
|
-
logger.error line
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def handle_ws_error(_error)
|
151
|
-
shutdown
|
152
|
-
end
|
153
|
-
|
154
|
-
def handle_ws_join
|
155
|
-
self.class.handle_event(:join, @req)
|
156
|
-
end
|
157
|
-
|
158
|
-
def handle_ws_close(_status, _message)
|
159
|
-
shutdown
|
160
|
-
end
|
161
|
-
|
162
|
-
def self.handle_event(event, req)
|
163
|
-
context = CallContext.new(req.env)
|
164
|
-
|
165
|
-
event_handlers(event).each do |block|
|
166
|
-
context.instance_exec(&block)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def self.event_handlers(event = nil)
|
171
|
-
@event_handlers.fetch(event, [])
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
class Parser
|
176
|
-
def initialize(version: nil)
|
177
|
-
@parser = WebSocket::Frame::Incoming::Server.new(version: version)
|
178
|
-
@handlers = {}
|
179
|
-
end
|
180
|
-
|
181
|
-
def on(event, &block)
|
182
|
-
@handlers[event] = block
|
183
|
-
end
|
184
|
-
|
185
|
-
def <<(data)
|
186
|
-
@parser << data
|
187
|
-
process
|
188
|
-
end
|
189
|
-
|
190
|
-
private
|
191
|
-
|
192
|
-
def process
|
193
|
-
while (frame = @parser.next)
|
194
|
-
case frame.type
|
195
|
-
when :text
|
196
|
-
handle :message, frame
|
197
|
-
when :close
|
198
|
-
handle :close, frame
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
def handle(event, frame)
|
204
|
-
return unless @handlers.keys.include?(event)
|
205
|
-
@handlers[event].call(frame.data)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require 'securerandom'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
require 'pakyow/support'
|
5
|
-
require 'pakyow/core'
|
6
|
-
require 'pakyow/realtime/helpers'
|
7
|
-
require 'pakyow/realtime/hooks'
|
8
|
-
require 'pakyow/realtime/context'
|
9
|
-
require 'pakyow/realtime/delegate'
|
10
|
-
require 'pakyow/realtime/registries/simple_registry'
|
11
|
-
require 'pakyow/realtime/registries/redis_registry'
|
12
|
-
require 'pakyow/realtime/redis_subscription'
|
13
|
-
require 'pakyow/realtime/websocket'
|
14
|
-
require 'pakyow/realtime/config'
|
15
|
-
require 'pakyow/realtime/exceptions'
|
16
|
-
require 'pakyow/realtime/message_handler'
|
17
|
-
require 'pakyow/realtime/message_handlers/call_route'
|
18
|
-
require 'pakyow/realtime/message_handlers/ping'
|
19
|
-
require 'pakyow/realtime/ext/request'
|
@@ -1 +0,0 @@
|
|
1
|
-
require 'pakyow/realtime'
|