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.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8bd4cd2aae232d43ce4107dc2641901c64470a1d
4
- data.tar.gz: 76f0933e09a694a9092b60d94390942a0acc997f
2
+ SHA256:
3
+ metadata.gz: e741f872b4f4c8c70d0b0435f69dea7a686e1d8ba95b3416ae89b8f3a50bd6a2
4
+ data.tar.gz: da45c2b4b7238ab709aeb7a243d6e2ee3f6869a86fc0d4d99e02a701960cc56a
5
5
  SHA512:
6
- metadata.gz: 9c67add106eb7537aee27d8c9ffb004fbc0f57f60b4de299a2954236b356d135c45a120a080dce548e61506490f4368b0e0aafdda61ce6868eaf3bf5b14add02
7
- data.tar.gz: ea16f4f8352e935015b8e175e1502d2fa1c683b1249a1cc3e7724da2dfa18188b3ff9b75c793950562537a6733dad24d59ae291ecf394bb5d0f199db3afa1411
6
+ metadata.gz: a44fd08cb285611e91b7129d266885ad30e6cecc02b33ba3b7bbacb36cee82228515437b027f305fa7cedd03af4c0c502be4a3b7d95ba9d0da83d60dc3b31576
7
+ data.tar.gz: 9524a3586f595db6a0c9d4f9538b9f8424be88aa627f8ad8f91030d1157ef9e84ee242ef9ca48ac2405f443a5a70d8b787e265c60c4ee4ca703064a0fb0dd1c4
@@ -1,3 +1,8 @@
1
+ # 1.0.0
2
+
3
+ * IMPROVED WebSocket reliability and performance.
4
+ * CHANGED WebSocket log level to verbose.
5
+
1
6
  # 0.11.0
2
7
 
3
8
  * Gracefully shuts down WebSocket when something bad happens
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) Metabahn, LLC
2
+
3
+ Pakyow Realtime is an open-source project licensed under the terms of the LGPLv3 license.
4
+ See <https://choosealicense.com/licenses/lgpl-3.0/> for license text.
@@ -146,8 +146,7 @@ Source code can be downloaded as part of the Pakyow project on Github:
146
146
 
147
147
  # License
148
148
 
149
- Pakyow Realtime is released free and open-source under the [MIT
150
- License](http://opensource.org/licenses/MIT).
149
+ Pakyow Realtime is free and open-source under the [LGPLv3 license](https://choosealicense.com/licenses/lgpl-3.0/).
151
150
 
152
151
  # Support
153
152
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Environment
7
+ module Realtime
8
+ module Config
9
+ extend Support::Extension
10
+
11
+ apply_extension do
12
+ configurable :realtime do
13
+ setting :server, true
14
+
15
+ setting :adapter, :memory
16
+ setting :adapter_settings, {}
17
+
18
+ defaults :production do
19
+ setting :adapter, :redis
20
+ setting :adapter_settings do
21
+ Pakyow.config.redis.to_h
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/message_verifier"
4
+
5
+ require "pakyow/realtime/websocket"
6
+
7
+ module Pakyow
8
+ module Realtime
9
+ module Actions
10
+ class Upgrader
11
+ def call(connection)
12
+ if websocket?(connection)
13
+ WebSocket.new(connection.verifier.verify(connection.params[:id]), connection)
14
+ connection.halt
15
+ end
16
+ rescue Support::MessageVerifier::TamperedMessage
17
+ connection.status = 403
18
+ connection.halt
19
+ end
20
+
21
+ private
22
+
23
+ def websocket?(connection)
24
+ connection.path == "/pw-socket"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Realtime
7
+ module Behavior
8
+ module Config
9
+ extend Support::Extension
10
+
11
+ apply_extension do
12
+ configurable :realtime do
13
+ setting :adapter_settings, {}
14
+ setting :path, "pw-socket"
15
+ setting :endpoint
16
+ setting :log_initial_request, false
17
+
18
+ defaults :production do
19
+ setting :adapter_settings do
20
+ { key_prefix: [Pakyow.config.redis.key_prefix, config.name].join("/") }
21
+ end
22
+
23
+ setting :log_initial_request, true
24
+ end
25
+
26
+ configurable :timeouts do
27
+ # Give sockets 60 seconds to connect before cleaning up their state.
28
+ #
29
+ setting :initial, 60
30
+
31
+ # When a socket disconnects, keep state around for 24 hours before
32
+ # cleaning up. This improves the user experience in cases such as
33
+ # when a browser window is left open on a sleeping computer.
34
+ #
35
+ setting :disconnect, 24 * 60 * 60
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ require "pakyow/presenter/view"
6
+
7
+ module Pakyow
8
+ module Realtime
9
+ module Behavior
10
+ module Rendering
11
+ module InstallWebsocket
12
+ extend Support::Extension
13
+
14
+ apply_extension do
15
+ build do |view|
16
+ if head = view.head
17
+ head.append(Support::SafeStringHelpers.html_safe("<meta name=\"pw-socket\" ui=\"socket\">"))
18
+ end
19
+ end
20
+
21
+ attach do |presenter, app:|
22
+ presenter.render node: -> {
23
+ node = object.each_significant_node(:meta).find { |meta_node|
24
+ meta_node.attributes[:name] == "pw-socket"
25
+ }
26
+
27
+ unless node.nil?
28
+ Presenter::View.from_object(node)
29
+ end
30
+ } do
31
+ endpoint = app.config.realtime.endpoint
32
+
33
+ unless endpoint
34
+ endpoint = if Pakyow.config.server.proxy
35
+ # Connect directly to the app in development, since the proxy does not support websocket connections.
36
+ #
37
+ File.join("ws://#{Pakyow.config.server.host}:#{Pakyow.config.server.port}", app.config.realtime.path)
38
+ else
39
+ File.join("#{presentables[:__ws_protocol]}://#{presentables[:__ws_authority]}", app.config.realtime.path)
40
+ end
41
+ end
42
+
43
+ attributes["data-ui"] = "socket(endpoint: #{endpoint}?id=#{presentables[:__verifier].sign(presentables[:__socket_client_id])})"
44
+ end
45
+ end
46
+
47
+ expose do |connection|
48
+ connection.set(:__verifier, connection.verifier)
49
+ connection.set(:__ws_protocol, connection.secure? ? "wss" : "ws")
50
+ connection.set(:__ws_authority, connection.authority)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ require "pakyow/support/extension"
6
+ require "pakyow/support/serializer"
7
+
8
+ module Pakyow
9
+ module Realtime
10
+ module Behavior
11
+ # Persists the in-memory realtime server across restarts.
12
+ #
13
+ module Serialization
14
+ extend Support::Extension
15
+
16
+ apply_extension do
17
+ on "shutdown", priority: :high do
18
+ if Pakyow.config.realtime.adapter == :memory && instance_variable_defined?(:@websocket_server)
19
+ realtime_server_serializer.serialize
20
+ end
21
+ end
22
+
23
+ after "boot" do
24
+ if Pakyow.config.realtime.adapter == :memory
25
+ realtime_server_serializer.deserialize
26
+ end
27
+ end
28
+ end
29
+
30
+ private def realtime_server_serializer
31
+ Support::Serializer.new(
32
+ @websocket_server.adapter,
33
+ name: "#{config.name}-realtime",
34
+ path: File.join(
35
+ Pakyow.config.root, "tmp", "state"
36
+ )
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/deep_freeze"
4
+ require "pakyow/support/extension"
5
+
6
+ require "pakyow/realtime/server"
7
+
8
+ module Pakyow
9
+ module Realtime
10
+ module Behavior
11
+ module Server
12
+ extend Support::Extension
13
+
14
+ apply_extension do
15
+ extend Support::DeepFreeze
16
+ unfreezable :websocket_server
17
+ attr_reader :websocket_server
18
+
19
+ after "initialize", priority: :high do
20
+ @websocket_server = if is_a?(Plugin)
21
+ parent.websocket_server
22
+ else
23
+ Realtime::Server.new(
24
+ Pakyow.config.realtime.adapter,
25
+ Pakyow.config.realtime.adapter_settings.to_h.merge(
26
+ config.realtime.adapter_settings.to_h
27
+ ),
28
+ config.realtime.timeouts
29
+ )
30
+ end
31
+ end
32
+
33
+ on "shutdown" do
34
+ if instance_variable_defined?(:@websocket_server)
35
+ @websocket_server.shutdown
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Realtime
7
+ module Behavior
8
+ # Silences asset requests from being logged.
9
+ #
10
+ module Silencing
11
+ extend Support::Extension
12
+
13
+ apply_extension do
14
+ on "load" do
15
+ unless config.realtime.log_initial_request
16
+ Pakyow.silence do |connection|
17
+ connection.path.start_with?(File.join("/", config.realtime.path))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Realtime
5
+ class Channel
6
+ class << self
7
+ def parse(qualified_channel)
8
+ Channel.new(*qualified_channel.split("::", 2))
9
+ end
10
+ end
11
+
12
+ attr_reader :name, :qualifier
13
+
14
+ def initialize(channel_name, qualifier = nil)
15
+ @name, @qualifier = channel_name, qualifier
16
+ end
17
+
18
+ def to_s
19
+ [@name, @qualifier].join("::")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Realtime
5
+ # Deals with realtime connections in context of an app. Instances are
6
+ # returned by the `socket` helper method during routing.
7
+ #
8
+ # @api public
9
+ class Context
10
+ # @api private
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ # Push a message down one or more channels.
16
+ #
17
+ # @api public
18
+ def push(msg, *channels)
19
+ delegate.push(msg, *channels)
20
+ end
21
+
22
+ # Push a message down a channel directed at a specific client,
23
+ # identified by key.
24
+ #
25
+ # @api public
26
+ def push_to_key(msg, channel, key, propagated: false)
27
+ delegate.push_to_key(msg, channel, key, propagated: propagated)
28
+ end
29
+
30
+ # Returns an instance of the connection delegate.
31
+ #
32
+ # @api private
33
+ def delegate
34
+ Delegate.instance
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/framework"
4
+
5
+ require "pakyow/realtime/helpers/broadcasting"
6
+ require "pakyow/realtime/helpers/subscriptions"
7
+ require "pakyow/realtime/helpers/socket"
8
+
9
+ require "pakyow/realtime/behavior/config"
10
+ require "pakyow/realtime/behavior/serialization"
11
+ require "pakyow/realtime/behavior/server"
12
+ require "pakyow/realtime/behavior/silencing"
13
+
14
+ require "pakyow/realtime/actions/upgrader"
15
+
16
+ require "pakyow/realtime/behavior/rendering/install_websocket"
17
+
18
+ module Pakyow
19
+ module Realtime
20
+ class Framework < Pakyow::Framework(:realtime)
21
+ def boot
22
+ object.class_eval do
23
+ register_helper :active, Helpers::Broadcasting
24
+ register_helper :active, Helpers::Subscriptions
25
+ register_helper :passive, Helpers::Socket
26
+
27
+ # Socket events are triggered on the app.
28
+ #
29
+ events :join, :leave
30
+
31
+ include Behavior::Config
32
+ include Behavior::Server
33
+ include Behavior::Silencing
34
+ include Behavior::Serialization
35
+
36
+ isolated :Renderer do
37
+ include Behavior::Rendering::InstallWebsocket
38
+ end
39
+
40
+ isolated :Connection do
41
+ after "initialize" do
42
+ set(:__socket_client_id, params[:socket_client_id] || Support::MessageVerifier.key)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Realtime
5
+ module Helpers
6
+ module Broadcasting
7
+ def broadcast(message)
8
+ app.websocket_server.subscription_broadcast(socket_client_id, message)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Realtime
5
+ module Helpers
6
+ module Socket
7
+ def socket_client_id
8
+ @connection.get(:__socket_client_id)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/realtime/channel"
4
+
5
+ module Pakyow
6
+ module Realtime
7
+ module Helpers
8
+ module Subscriptions
9
+ def subscribe(channel, *qualifiers)
10
+ channels = if qualifiers.empty?
11
+ Channel.new(channel)
12
+ else
13
+ qualifiers.map { |qualifier|
14
+ Channel.new(channel, qualifier)
15
+ }
16
+ end
17
+
18
+ app.websocket_server.socket_subscribe(socket_client_id, *channels)
19
+ end
20
+
21
+ def unsubscribe(channel, *qualifiers)
22
+ channels = if qualifiers.empty?
23
+ Channel.new(channel, "*")
24
+ else
25
+ qualifiers.map { |qualifier|
26
+ Channel.new(channel, qualifier)
27
+ }
28
+ end
29
+
30
+ app.websocket_server.socket_unsubscribe(*channels)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/array"
4
+ require "concurrent/hash"
5
+ require "concurrent/scheduled_task"
6
+
7
+ module Pakyow
8
+ module Realtime
9
+ class Server
10
+ module Adapters
11
+ # Manages websocket channels in memory.
12
+ #
13
+ # Great for development, not for use in production!
14
+ #
15
+ # @api private
16
+ class Memory
17
+ def initialize(server, _config)
18
+ @server = server
19
+
20
+ @socket_ids_by_channel = Concurrent::Hash.new
21
+ @channels_by_socket_id = Concurrent::Hash.new
22
+ @expirations_for_socket_id = Concurrent::Hash.new
23
+ @socket_instances_by_socket_id = Concurrent::Hash.new
24
+ end
25
+
26
+ def connect
27
+ # intentionally empty
28
+ end
29
+
30
+ def disconnect
31
+ # intentionally empty
32
+ end
33
+
34
+ def socket_subscribe(socket_id, *channels)
35
+ channels.each do |channel|
36
+ channel = channel.to_s.to_sym
37
+ (@socket_ids_by_channel[channel] ||= Concurrent::Array.new) << socket_id
38
+ (@channels_by_socket_id[socket_id] ||= Concurrent::Array.new) << channel
39
+ end
40
+ end
41
+
42
+ def socket_unsubscribe(*channels)
43
+ channels.each do |channel|
44
+ channel = Regexp.new(channel.to_s)
45
+
46
+ @socket_ids_by_channel.select { |key|
47
+ key.to_s.match?(channel)
48
+ }.each do |key, socket_ids|
49
+ @socket_ids_by_channel.delete(key)
50
+
51
+ socket_ids.each do |socket_id|
52
+ @channels_by_socket_id[socket_id]&.delete(key)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def subscription_broadcast(channel, message)
59
+ @server.transmit_message_to_connection_ids(message, socket_ids_for_channel(channel))
60
+ end
61
+
62
+ def expire(socket_id, seconds)
63
+ task = Concurrent::ScheduledTask.execute(seconds) {
64
+ channels_for_socket_id(socket_id).each do |channel|
65
+ @channels_by_socket_id.delete(socket_id)
66
+ @socket_ids_by_channel[channel].delete(socket_id)
67
+ @socket_instances_by_socket_id.delete(socket_id)
68
+ end
69
+ }
70
+
71
+ @expirations_for_socket_id[socket_id] ||= []
72
+ @expirations_for_socket_id[socket_id] << task
73
+ end
74
+
75
+ def persist(socket_id)
76
+ (@expirations_for_socket_id[socket_id] || []).each(&:cancel)
77
+ @expirations_for_socket_id.delete(socket_id)
78
+ end
79
+
80
+ def expiring?(socket_id)
81
+ @expirations_for_socket_id[socket_id]&.any?
82
+ end
83
+
84
+ def current!(socket_id, socket_instance_id)
85
+ @socket_instances_by_socket_id[socket_id] = socket_instance_id
86
+ end
87
+
88
+ def current?(socket_id, socket_instance_id)
89
+ @socket_instances_by_socket_id[socket_id] == socket_instance_id
90
+ end
91
+
92
+ SERIALIZABLE_IVARS = %i(
93
+ @socket_ids_by_channel
94
+ @channels_by_socket_id
95
+ @socket_instances_by_socket_id
96
+ ).freeze
97
+
98
+ def serialize
99
+ SERIALIZABLE_IVARS.each_with_object({}) do |ivar, hash|
100
+ hash[ivar] = instance_variable_get(ivar)
101
+ end
102
+ end
103
+
104
+ protected
105
+
106
+ def socket_ids_for_channel(channel)
107
+ channel = Regexp.new(channel.to_s)
108
+
109
+ @socket_ids_by_channel.select { |key|
110
+ key.to_s.match?(channel)
111
+ }.each_with_object([]) do |(_, socket_ids_for_channel), socket_ids|
112
+ socket_ids.concat(
113
+ socket_ids_for_channel.reject { |socket_id|
114
+ expiring?(socket_id)
115
+ }
116
+ )
117
+ end
118
+ end
119
+
120
+ def channels_for_socket_id(socket_id)
121
+ @channels_by_socket_id[socket_id] || []
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end