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
@@ -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
|