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.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/{pakyow-realtime/CHANGELOG.md → CHANGELOG.md} +5 -0
  3. data/LICENSE +4 -0
  4. data/{pakyow-realtime/README.md → README.md} +1 -2
  5. data/lib/pakyow/environment/realtime/config.rb +29 -0
  6. data/lib/pakyow/realtime/actions/upgrader.rb +29 -0
  7. data/lib/pakyow/realtime/behavior/config.rb +42 -0
  8. data/lib/pakyow/realtime/behavior/rendering/install_websocket.rb +57 -0
  9. data/lib/pakyow/realtime/behavior/serialization.rb +42 -0
  10. data/lib/pakyow/realtime/behavior/server.rb +42 -0
  11. data/lib/pakyow/realtime/behavior/silencing.rb +25 -0
  12. data/lib/pakyow/realtime/channel.rb +23 -0
  13. data/lib/pakyow/realtime/context.rb +38 -0
  14. data/lib/pakyow/realtime/framework.rb +49 -0
  15. data/lib/pakyow/realtime/helpers/broadcasting.rb +13 -0
  16. data/lib/pakyow/realtime/helpers/socket.rb +13 -0
  17. data/lib/pakyow/realtime/helpers/subscriptions.rb +35 -0
  18. data/lib/pakyow/realtime/server/adapters/memory.rb +127 -0
  19. data/lib/pakyow/realtime/server/adapters/redis.rb +277 -0
  20. data/lib/pakyow/realtime/server.rb +152 -0
  21. data/lib/pakyow/realtime/websocket.rb +157 -0
  22. data/lib/pakyow/realtime.rb +13 -0
  23. metadata +73 -44
  24. data/pakyow-realtime/LICENSE +0 -20
  25. data/pakyow-realtime/lib/pakyow/realtime/config.rb +0 -20
  26. data/pakyow-realtime/lib/pakyow/realtime/connection.rb +0 -18
  27. data/pakyow-realtime/lib/pakyow/realtime/context.rb +0 -68
  28. data/pakyow-realtime/lib/pakyow/realtime/delegate.rb +0 -112
  29. data/pakyow-realtime/lib/pakyow/realtime/exceptions.rb +0 -6
  30. data/pakyow-realtime/lib/pakyow/realtime/ext/request.rb +0 -10
  31. data/pakyow-realtime/lib/pakyow/realtime/helpers.rb +0 -40
  32. data/pakyow-realtime/lib/pakyow/realtime/hooks.rb +0 -41
  33. data/pakyow-realtime/lib/pakyow/realtime/message_handler.rb +0 -57
  34. data/pakyow-realtime/lib/pakyow/realtime/message_handlers/call_route.rb +0 -34
  35. data/pakyow-realtime/lib/pakyow/realtime/message_handlers/ping.rb +0 -8
  36. data/pakyow-realtime/lib/pakyow/realtime/redis_subscription.rb +0 -61
  37. data/pakyow-realtime/lib/pakyow/realtime/registries/redis_registry.rb +0 -107
  38. data/pakyow-realtime/lib/pakyow/realtime/registries/simple_registry.rb +0 -40
  39. data/pakyow-realtime/lib/pakyow/realtime/websocket.rb +0 -209
  40. data/pakyow-realtime/lib/pakyow/realtime.rb +0 -19
  41. 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'