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