pakyow-realtime 0.11.3 → 1.0.0.rc1
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 +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'
|