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
@@ -0,0 +1,277 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis"
|
4
|
+
require "concurrent/array"
|
5
|
+
require "concurrent/timer_task"
|
6
|
+
require "connection_pool"
|
7
|
+
|
8
|
+
module Pakyow
|
9
|
+
module Realtime
|
10
|
+
class Server
|
11
|
+
module Adapters
|
12
|
+
# Manages websocket channels in redis.
|
13
|
+
#
|
14
|
+
# Use this in production.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
class Redis
|
18
|
+
KEY_PART_SEPARATOR = "/"
|
19
|
+
KEY_PREFIX = "realtime"
|
20
|
+
INFINITY = "+inf"
|
21
|
+
|
22
|
+
PUBSUB_PREFIX = "pubsub"
|
23
|
+
|
24
|
+
def initialize(server, config)
|
25
|
+
@server, @config = server, config
|
26
|
+
@prefix = [@config[:key_prefix], KEY_PREFIX].join(KEY_PART_SEPARATOR)
|
27
|
+
|
28
|
+
connect
|
29
|
+
cleanup
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
@redis = ConnectionPool.new(**@config[:pool]) {
|
34
|
+
::Redis.new(@config[:connection])
|
35
|
+
}
|
36
|
+
|
37
|
+
@buffer = Buffer.new(@redis, pubsub_channel)
|
38
|
+
@subscriber = Subscriber.new(::Redis.new(@config[:connection]), pubsub_channel) do |payload|
|
39
|
+
channel, message = Marshal.restore(payload).values_at(:channel, :message)
|
40
|
+
@server.transmit_message_to_connection_ids(message, socket_ids_for_channel(channel), raw: true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def disconnect
|
45
|
+
@subscriber.disconnect
|
46
|
+
end
|
47
|
+
|
48
|
+
def socket_subscribe(socket_id, *channels)
|
49
|
+
@redis.with do |redis|
|
50
|
+
redis.multi do |transaction|
|
51
|
+
channels.each do |channel|
|
52
|
+
channel = channel.to_s
|
53
|
+
transaction.zadd(key_socket_ids_by_channel(channel), INFINITY, socket_id)
|
54
|
+
transaction.zadd(key_channels_by_socket_id(socket_id), INFINITY, channel)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def socket_unsubscribe(*channels)
|
61
|
+
@redis.with do |redis|
|
62
|
+
channels.each do |channel|
|
63
|
+
channel = channel.to_s
|
64
|
+
|
65
|
+
# Channel could contain a wildcard, so this takes some work...
|
66
|
+
redis.scan_each(match: key_socket_ids_by_channel(channel)) do |key|
|
67
|
+
channel = key.split("channel:", 2)[1]
|
68
|
+
|
69
|
+
socket_ids = redis.zrangebyscore(
|
70
|
+
key, Time.now.to_i, INFINITY
|
71
|
+
)
|
72
|
+
|
73
|
+
redis.multi do |transaction|
|
74
|
+
transaction.del(key)
|
75
|
+
|
76
|
+
socket_ids.each do |socket_id|
|
77
|
+
transaction.zrem(key_channels_by_socket_id(socket_id), channel)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def subscription_broadcast(channel, message)
|
86
|
+
@buffer << Marshal.dump(channel: channel, message: { payload: message }.to_json)
|
87
|
+
end
|
88
|
+
|
89
|
+
def expire(socket_id, seconds)
|
90
|
+
time_expire = Time.now.to_i + seconds
|
91
|
+
channels = channels_for_socket_id(socket_id)
|
92
|
+
|
93
|
+
@redis.with do |redis|
|
94
|
+
redis.multi do |transaction|
|
95
|
+
channels.each do |channel|
|
96
|
+
transaction.zadd(key_socket_ids_by_channel(channel), time_expire, socket_id)
|
97
|
+
end
|
98
|
+
|
99
|
+
transaction.expireat(key_channels_by_socket_id(socket_id), time_expire + 1)
|
100
|
+
transaction.expireat(key_socket_instance_id_by_socket_id(socket_id), time_expire + 1)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def persist(socket_id)
|
106
|
+
channels = channels_for_socket_id(socket_id)
|
107
|
+
|
108
|
+
@redis.with do |redis|
|
109
|
+
redis.multi do |transaction|
|
110
|
+
channels.each do |channel|
|
111
|
+
transaction.zadd(key_socket_ids_by_channel(channel), INFINITY, socket_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
transaction.persist(key_channels_by_socket_id(socket_id))
|
115
|
+
transaction.persist(key_socket_instance_id_by_socket_id(socket_id))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def current!(socket_id, socket_instance_id)
|
121
|
+
@redis.with do |redis|
|
122
|
+
redis.set(key_socket_instance_id_by_socket_id(socket_id), socket_instance_id)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def current?(socket_id, socket_instance_id)
|
127
|
+
@redis.with do |redis|
|
128
|
+
redis.get(key_socket_instance_id_by_socket_id(socket_id)) == socket_instance_id.to_s
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
def socket_ids_for_channel(channel)
|
135
|
+
@redis.with do |redis|
|
136
|
+
redis.zrangebyscore(
|
137
|
+
key_socket_ids_by_channel(channel), INFINITY, INFINITY
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def channels_for_socket_id(socket_id)
|
143
|
+
@redis.with do |redis|
|
144
|
+
redis.zrangebyscore(
|
145
|
+
key_channels_by_socket_id(socket_id), INFINITY, INFINITY
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_key(*parts)
|
151
|
+
[@prefix].concat(parts).join(KEY_PART_SEPARATOR)
|
152
|
+
end
|
153
|
+
|
154
|
+
def key_socket_ids_by_channel(channel)
|
155
|
+
build_key("channel:#{channel}")
|
156
|
+
end
|
157
|
+
|
158
|
+
def key_channels_by_socket_id(socket_id)
|
159
|
+
build_key("socket_id:#{socket_id}")
|
160
|
+
end
|
161
|
+
|
162
|
+
def key_socket_instance_id_by_socket_id(socket_id)
|
163
|
+
build_key("socket_instance_id:#{socket_id}")
|
164
|
+
end
|
165
|
+
|
166
|
+
def pubsub_channel
|
167
|
+
[@prefix, PUBSUB_PREFIX].join(KEY_PART_SEPARATOR)
|
168
|
+
end
|
169
|
+
|
170
|
+
def cleanup
|
171
|
+
Concurrent::TimerTask.new(execution_interval: 300, timeout_interval: 300) {
|
172
|
+
Pakyow.logger.debug "[Pakyow::Realtime::Server::Adapters::Redis] Cleaning up channel keys"
|
173
|
+
|
174
|
+
removed_count = 0
|
175
|
+
@redis.with do |redis|
|
176
|
+
redis.scan_each(match: key_socket_ids_by_channel("*")) do |key|
|
177
|
+
socket_ids = redis.zrangebyscore(
|
178
|
+
key, Time.now.to_i, INFINITY
|
179
|
+
)
|
180
|
+
|
181
|
+
if socket_ids.empty?
|
182
|
+
removed_count += 1
|
183
|
+
redis.del(key)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
Pakyow.logger.debug "[Pakyow::Realtime::Server::Adapters::Redis] Removed #{removed_count} keys"
|
189
|
+
}.execute
|
190
|
+
end
|
191
|
+
|
192
|
+
class Buffer
|
193
|
+
# The number of publish commands to pipeline to redis.
|
194
|
+
#
|
195
|
+
PUBLISH_BUFFER_SIZE = 1_000
|
196
|
+
|
197
|
+
# How often the publish buffer should be flushed.
|
198
|
+
#
|
199
|
+
PUBLISH_BUFFER_FLUSH_MS = 150
|
200
|
+
|
201
|
+
def initialize(redis, channel)
|
202
|
+
@redis, @channel = redis, channel
|
203
|
+
@buffer = Concurrent::Array.new
|
204
|
+
end
|
205
|
+
|
206
|
+
def <<(payload)
|
207
|
+
@buffer << payload
|
208
|
+
maybe_flush
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
|
213
|
+
def maybe_flush
|
214
|
+
if @buffer.count > PUBLISH_BUFFER_SIZE
|
215
|
+
flush
|
216
|
+
end
|
217
|
+
|
218
|
+
unless @task&.pending?
|
219
|
+
@task = Concurrent::ScheduledTask.execute(PUBLISH_BUFFER_FLUSH_MS / 1_000) {
|
220
|
+
flush
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def flush
|
226
|
+
@redis.with do |redis|
|
227
|
+
redis.pipelined do |pipeline|
|
228
|
+
until @buffer.empty?
|
229
|
+
pipeline.publish(@channel, @buffer.shift)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Subscriber
|
237
|
+
def initialize(redis, channel, &callback)
|
238
|
+
@redis, @channel, @callback = redis, channel, callback
|
239
|
+
|
240
|
+
@thread = Thread.new do
|
241
|
+
subscribe
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def disconnect
|
246
|
+
@thread.exit
|
247
|
+
@redis.disconnect!
|
248
|
+
end
|
249
|
+
|
250
|
+
def subscribe
|
251
|
+
@redis.subscribe(@channel) do |on|
|
252
|
+
on.message do |_, payload|
|
253
|
+
begin
|
254
|
+
@callback.call(payload)
|
255
|
+
rescue => error
|
256
|
+
Pakyow.logger.error "[Pakyow::Realtime::Server::Adapters::Redis] Subscriber callback failed: #{error}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue ::Redis::CannotConnectError
|
261
|
+
Pakyow.logger.error "[Pakyow::Realtime::Server::Adapters::Redis] Subscriber disconnected"
|
262
|
+
resubscribe
|
263
|
+
rescue => error
|
264
|
+
Pakyow.logger.error "[Pakyow::Realtime::Server::Adapters::Redis] Subscriber crashed: #{error}"
|
265
|
+
resubscribe
|
266
|
+
end
|
267
|
+
|
268
|
+
def resubscribe
|
269
|
+
sleep 0.25
|
270
|
+
subscribe
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/array"
|
4
|
+
require "concurrent/timer_task"
|
5
|
+
require "concurrent/executor/thread_pool_executor"
|
6
|
+
|
7
|
+
require "pakyow/support/message_verifier"
|
8
|
+
|
9
|
+
require "pakyow/realtime/websocket"
|
10
|
+
|
11
|
+
module Pakyow
|
12
|
+
module Realtime
|
13
|
+
class Server
|
14
|
+
attr_reader :adapter
|
15
|
+
|
16
|
+
HEARTBEAT_INTERVAL = 3
|
17
|
+
|
18
|
+
def initialize(adapter = :memory, adapter_config, timeout_config)
|
19
|
+
require "pakyow/realtime/server/adapters/#{adapter}"
|
20
|
+
@adapter = Adapters.const_get(adapter.to_s.capitalize).new(self, adapter_config)
|
21
|
+
@sockets = Concurrent::Array.new
|
22
|
+
@timeout_config = timeout_config
|
23
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
24
|
+
auto_terminate: false,
|
25
|
+
min_threads: 1,
|
26
|
+
max_threads: 10,
|
27
|
+
max_queue: 0
|
28
|
+
)
|
29
|
+
|
30
|
+
connect
|
31
|
+
rescue LoadError => e
|
32
|
+
Pakyow.logger.error "Failed to load data subscriber store adapter named `#{adapter}'"
|
33
|
+
Pakyow.logger.error e.message
|
34
|
+
end
|
35
|
+
|
36
|
+
def shutdown
|
37
|
+
disconnect
|
38
|
+
@executor.shutdown
|
39
|
+
@executor.wait_for_termination(30)
|
40
|
+
end
|
41
|
+
|
42
|
+
def connect
|
43
|
+
@executor << -> {
|
44
|
+
start_heartbeat; @adapter.connect
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def disconnect
|
49
|
+
@executor << -> {
|
50
|
+
stop_heartbeat; @adapter.disconnect
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def socket_connect(id_or_socket)
|
55
|
+
@executor << -> {
|
56
|
+
find_socket(id_or_socket) do |socket|
|
57
|
+
@sockets << socket
|
58
|
+
@adapter.persist(socket.id)
|
59
|
+
@adapter.current!(socket.id, socket.object_id)
|
60
|
+
end
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def socket_disconnect(id_or_socket)
|
65
|
+
@executor << -> {
|
66
|
+
find_socket(id_or_socket) do |socket|
|
67
|
+
@sockets.delete(socket)
|
68
|
+
|
69
|
+
# If this isn't the current instance for the socket id, it means that a
|
70
|
+
# reconnect probably happened and the new socket connected before we
|
71
|
+
# knew that the old one disconnected. Since there's a newer socket,
|
72
|
+
# don't trigger leave events or expirations for the old one.
|
73
|
+
#
|
74
|
+
if @adapter.current?(socket.id, socket.object_id)
|
75
|
+
socket.leave
|
76
|
+
@adapter.expire(socket.id, @timeout_config.disconnect)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def socket_subscribe(id_or_socket, *channels)
|
83
|
+
@executor << -> {
|
84
|
+
find_socket_id(id_or_socket) do |socket_id|
|
85
|
+
@adapter.socket_subscribe(socket_id, *channels)
|
86
|
+
@adapter.expire(socket_id, @timeout_config.initial)
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def socket_unsubscribe(*channels)
|
92
|
+
@executor << -> {
|
93
|
+
@adapter.socket_unsubscribe(*channels)
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def subscription_broadcast(channel, message)
|
98
|
+
@executor << -> {
|
99
|
+
@adapter.subscription_broadcast(channel.to_s, channel: channel.name, message: message)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Called by the adapter, which guarantees that this server has connections for these ids.
|
104
|
+
#
|
105
|
+
def transmit_message_to_connection_ids(message, socket_ids, raw: false)
|
106
|
+
socket_ids.each do |socket_id|
|
107
|
+
find_socket_by_id(socket_id)&.transmit(message, raw: raw)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_socket_by_id(socket_id)
|
112
|
+
@sockets.find { |socket| socket.id == socket_id }
|
113
|
+
end
|
114
|
+
|
115
|
+
def find_socket(id_or_socket)
|
116
|
+
socket = if id_or_socket.is_a?(WebSocket)
|
117
|
+
id_or_socket
|
118
|
+
else
|
119
|
+
find_socket_by_id(id_or_socket)
|
120
|
+
end
|
121
|
+
|
122
|
+
yield socket if socket
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_socket_id(id_or_socket)
|
126
|
+
socket_id = if id_or_socket.is_a?(WebSocket)
|
127
|
+
id_or_socket.id
|
128
|
+
else
|
129
|
+
id_or_socket
|
130
|
+
end
|
131
|
+
|
132
|
+
yield socket_id if socket_id
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def start_heartbeat
|
138
|
+
@heartbeat = Concurrent::TimerTask.new(execution_interval: HEARTBEAT_INTERVAL) do
|
139
|
+
@executor << -> {
|
140
|
+
@sockets.each(&:beat)
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
@heartbeat.execute
|
145
|
+
end
|
146
|
+
|
147
|
+
def stop_heartbeat
|
148
|
+
@heartbeat.shutdown
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require "pakyow/helpers/app"
|
7
|
+
require "pakyow/helpers/connection"
|
8
|
+
|
9
|
+
require "async/websocket"
|
10
|
+
|
11
|
+
require "protocol/websocket/connection"
|
12
|
+
require "protocol/websocket/headers"
|
13
|
+
|
14
|
+
module Pakyow
|
15
|
+
module Realtime
|
16
|
+
class WebSocket
|
17
|
+
Frame = ::Protocol::WebSocket::Frame
|
18
|
+
|
19
|
+
class Connection < ::Protocol::WebSocket::Connection
|
20
|
+
include ::Protocol::WebSocket::Headers
|
21
|
+
|
22
|
+
def self.call(framer, protocol = [], **options)
|
23
|
+
return self.new(framer, Array(protocol).first, **options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(framer, protocol = nil, **options)
|
27
|
+
super(framer, **options)
|
28
|
+
@protocol = protocol
|
29
|
+
end
|
30
|
+
|
31
|
+
attr :protocol
|
32
|
+
|
33
|
+
def call
|
34
|
+
self.close
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
include Pakyow::Helpers::App
|
39
|
+
include Pakyow::Helpers::Connection
|
40
|
+
|
41
|
+
attr_reader :id
|
42
|
+
|
43
|
+
def initialize(id, connection)
|
44
|
+
@id, @connection, @open = id, connection, false
|
45
|
+
@logger = Logger.new(:sock, id: @id[0..7], output: Pakyow.global_logger, level: Pakyow.config.logger.level)
|
46
|
+
@server = @connection.app.websocket_server
|
47
|
+
|
48
|
+
response = Async::WebSocket::Adapters::Native.open(@connection.request, handler: Connection) do |socket|
|
49
|
+
@socket = socket
|
50
|
+
|
51
|
+
handle_open
|
52
|
+
while message = socket.read
|
53
|
+
handle_message(message)
|
54
|
+
end
|
55
|
+
rescue EOFError, Protocol::WebSocket::ClosedError
|
56
|
+
ensure
|
57
|
+
@socket&.close; shutdown
|
58
|
+
end
|
59
|
+
|
60
|
+
@connection.__getobj__.instance_variable_set(:@response, response)
|
61
|
+
end
|
62
|
+
|
63
|
+
def open?
|
64
|
+
@open == true
|
65
|
+
end
|
66
|
+
|
67
|
+
def transmit(message, raw: false)
|
68
|
+
if open?
|
69
|
+
if raw
|
70
|
+
@socket.write(message)
|
71
|
+
else
|
72
|
+
@socket.write(JSON.dump(payload: message))
|
73
|
+
end
|
74
|
+
|
75
|
+
@socket.flush
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def beat
|
80
|
+
transmit("beat")
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def leave
|
85
|
+
trigger_presence(:leave)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def shutdown
|
91
|
+
if open?
|
92
|
+
@server.socket_disconnect(self)
|
93
|
+
@open = false
|
94
|
+
@logger.info "shutdown"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_open
|
99
|
+
@server.socket_connect(self)
|
100
|
+
@open = true
|
101
|
+
trigger_presence(:join)
|
102
|
+
@logger.info "opened"
|
103
|
+
transmit_system_info
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle_message(message)
|
107
|
+
@logger.internal("< " + message)
|
108
|
+
end
|
109
|
+
|
110
|
+
def trigger_presence(event)
|
111
|
+
@connection.app.hooks(:before, event).each do |hook, _|
|
112
|
+
instance_exec(&hook[:block])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def transmit_system_info
|
117
|
+
transmit(
|
118
|
+
channel: "system",
|
119
|
+
message: {
|
120
|
+
version: @connection.app.config.version
|
121
|
+
}
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module Async
|
129
|
+
module WebSocket
|
130
|
+
module Adapters
|
131
|
+
module Native
|
132
|
+
include ::Protocol::WebSocket::Headers
|
133
|
+
|
134
|
+
def self.websocket?(request)
|
135
|
+
request.headers.include?("upgrade")
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.open(request, headers: [], protocols: [], handler: Connection, **options)
|
139
|
+
if websocket?(request) && Array(request.protocol).include?(PROTOCOL)
|
140
|
+
# Select websocket sub-protocol:
|
141
|
+
if requested_protocol = request.headers[SEC_WEBSOCKET_PROTOCOL]
|
142
|
+
protocol = (requested_protocol & protocols).first
|
143
|
+
end
|
144
|
+
|
145
|
+
Response.for(request, headers, protocol: protocol, **options) do |stream|
|
146
|
+
framer = Protocol::WebSocket::Framer.new(stream)
|
147
|
+
|
148
|
+
yield handler.call(framer, protocol)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support"
|
4
|
+
require "pakyow/routing"
|
5
|
+
require "pakyow/presenter"
|
6
|
+
|
7
|
+
require "pakyow/realtime/framework"
|
8
|
+
|
9
|
+
require "pakyow/environment/realtime/config"
|
10
|
+
|
11
|
+
module Pakyow
|
12
|
+
include Environment::Realtime::Config
|
13
|
+
end
|