volcano-sdk 0.30.0 → 0.32.0
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 +4 -4
- data/lib/volcano/auth.rb +4 -48
- data/lib/volcano/auth_session_construction.rb +67 -0
- data/lib/volcano/auth_sign_up.rb +2 -0
- data/lib/volcano/realtime/broadcast_delivery.rb +26 -2
- data/lib/volcano/realtime/channel_callbacks.rb +19 -14
- data/lib/volcano/realtime/channel_lifecycle.rb +9 -9
- data/lib/volcano/realtime/connection.rb +9 -5
- data/lib/volcano/realtime/connection_callbacks.rb +5 -1
- data/lib/volcano/realtime/lifecycle.rb +2 -1
- data/lib/volcano/realtime/postgres_batch.rb +11 -3
- data/lib/volcano/realtime/postgres_changes.rb +31 -16
- data/lib/volcano/realtime/postgres_database.rb +14 -1
- data/lib/volcano/realtime/postgres_delivery.rb +6 -1
- data/lib/volcano/realtime/postgres_expansion.rb +10 -4
- data/lib/volcano/realtime/presence.rb +18 -4
- data/lib/volcano/realtime/presence_state.rb +23 -6
- data/lib/volcano/realtime/protocol.rb +14 -9
- data/lib/volcano/realtime/protocol_dispatch.rb +25 -7
- data/lib/volcano/realtime/protocol_io.rb +8 -4
- data/lib/volcano/realtime/protocol_lifecycle.rb +2 -0
- data/lib/volcano/realtime/protocol_publication_dispatch.rb +12 -10
- data/lib/volcano/realtime/protocol_recovery.rb +7 -3
- data/lib/volcano/realtime/protocol_recovery_dispatch.rb +2 -0
- data/lib/volcano/realtime/reconnect.rb +4 -1
- data/lib/volcano/realtime.rb +122 -99
- data/lib/volcano/storage.rb +18 -34
- data/lib/volcano/storage_models.rb +18 -5
- data/lib/volcano/storage_mutations.rb +8 -3
- data/lib/volcano/storage_object_response.rb +88 -0
- data/lib/volcano/storage_response.rb +92 -0
- data/lib/volcano/storage_resumable_upload.rb +13 -2
- data/lib/volcano/storage_session_adapter.rb +61 -0
- data/lib/volcano/storage_transport_adapter.rb +73 -0
- data/lib/volcano/storage_upload_models.rb +51 -27
- data/lib/volcano/storage_upload_sessions.rb +1 -35
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +6 -1
- data/sig/auth_credentials.rbs +15 -0
- data/sig/realtime.rbs +46 -0
- data/sig/realtime_postgres_change.rbs +1 -2
- data/sig/storage.rbs +29 -0
- data/sig/storage_records.rbs +3 -1
- data/sig/upload_session_status.rbs +3 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 551cf1235bb42130cc2c2689af7b4a68bad7582d2df5dc8345df72ee8d061925
|
|
4
|
+
data.tar.gz: 579af3b2c9301b7b22662e31706752c4d4098438d7ec3c25b6bb8029d17d6f4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3e37060a57939cf40d621880d6f9490af9912934b426d85485a37bd5ece0398d3a9adf0fc7034b211a21ef8b663fe6e2fd2aca6d4b7520cdfcc7373572b0d5f
|
|
7
|
+
data.tar.gz: f1d77ddbca839dfb7944e83cb025096dad93dd1fe846d13346e9825ec3280fede17d984b5ae14aed330ce4caa0f7a7eb0289107f985f089379da68a04095dce2
|
data/lib/volcano/auth.rb
CHANGED
|
@@ -9,9 +9,7 @@ module Volcano
|
|
|
9
9
|
include AuthUpdateUser
|
|
10
10
|
include AuthEmailChange
|
|
11
11
|
include AuthSessions
|
|
12
|
-
|
|
13
|
-
INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
|
|
14
|
-
private_constant :INCOMPLETE_SESSION
|
|
12
|
+
include AuthSessionConstruction
|
|
15
13
|
|
|
16
14
|
def initialize(client, transport, api_url:)
|
|
17
15
|
@client = client
|
|
@@ -27,10 +25,10 @@ module Volcano
|
|
|
27
25
|
@client.store_session(owned_complete_session(session), event: nil)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
def on_auth_state_change(&
|
|
31
|
-
raise ArgumentError, 'callback block required' unless
|
|
28
|
+
def on_auth_state_change(&)
|
|
29
|
+
raise ArgumentError, 'callback block required' unless block_given?
|
|
32
30
|
|
|
33
|
-
@client.subscribe_auth_state_change(&
|
|
31
|
+
@client.subscribe_auth_state_change(&)
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
def sign_in(email:, password:)
|
|
@@ -77,47 +75,5 @@ module Volcano
|
|
|
77
75
|
)
|
|
78
76
|
end
|
|
79
77
|
end
|
|
80
|
-
|
|
81
|
-
def build_session(payload)
|
|
82
|
-
owned_session(
|
|
83
|
-
access_token: payload.fetch('access_token'),
|
|
84
|
-
refresh_token: payload.fetch('refresh_token'),
|
|
85
|
-
user_id: payload.fetch('user').fetch('id'),
|
|
86
|
-
user: payload.fetch('user')
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def parse_refresh_session(payload)
|
|
91
|
-
owned_complete_session(build_session(payload))
|
|
92
|
-
rescue KeyError, TypeError, NoMethodError, ArgumentError => e
|
|
93
|
-
raise Error::TransportError, INCOMPLETE_SESSION, cause: e
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def complete_session?(session)
|
|
97
|
-
return false unless session.is_a?(Session)
|
|
98
|
-
|
|
99
|
-
values = [session.access_token, session.refresh_token, session.user_id]
|
|
100
|
-
values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
|
|
101
|
-
matching_session_user?(session)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def matching_session_user?(session)
|
|
105
|
-
session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def owned_complete_session(session)
|
|
109
|
-
raise ArgumentError, INCOMPLETE_SESSION unless complete_session?(session)
|
|
110
|
-
|
|
111
|
-
owned_session(**session.to_h)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def owned_session(access_token:, refresh_token:, user_id:, user: nil)
|
|
115
|
-
Session.new(
|
|
116
|
-
access_token: access_token.dup.freeze,
|
|
117
|
-
refresh_token: refresh_token.dup.freeze,
|
|
118
|
-
user_id: user_id.dup.freeze,
|
|
119
|
-
user: user
|
|
120
|
-
)
|
|
121
|
-
end
|
|
122
78
|
end
|
|
123
79
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Builds and validates owned credential snapshots for the auth facade.
|
|
5
|
+
module AuthSessionConstruction
|
|
6
|
+
INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
|
|
7
|
+
private_constant :INCOMPLETE_SESSION
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def build_session(payload)
|
|
12
|
+
raise TypeError, INCOMPLETE_SESSION unless payload.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
user = payload.fetch('user')
|
|
15
|
+
raise TypeError, INCOMPLETE_SESSION unless user.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
owned_session(
|
|
18
|
+
access_token: required_session_string(payload.fetch('access_token')),
|
|
19
|
+
refresh_token: required_session_string(payload.fetch('refresh_token')),
|
|
20
|
+
user_id: required_session_string(user.fetch('id')),
|
|
21
|
+
user: user
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse_refresh_session(payload)
|
|
26
|
+
owned_complete_session(build_session(payload))
|
|
27
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError => e
|
|
28
|
+
raise Error::TransportError, INCOMPLETE_SESSION, cause: e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def complete_session?(session)
|
|
32
|
+
values = [session.access_token, session.refresh_token, session.user_id]
|
|
33
|
+
values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
|
|
34
|
+
matching_session_user?(session)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def matching_session_user?(session)
|
|
38
|
+
session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def owned_complete_session(session)
|
|
42
|
+
raise ArgumentError, INCOMPLETE_SESSION unless session.is_a?(Session) && complete_session?(session)
|
|
43
|
+
|
|
44
|
+
owned_session(
|
|
45
|
+
access_token: required_session_string(session.access_token),
|
|
46
|
+
refresh_token: required_session_string(session.refresh_token),
|
|
47
|
+
user_id: required_session_string(session.user_id),
|
|
48
|
+
user: session.user
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def required_session_string(value)
|
|
53
|
+
return value if value.is_a?(String) && !value.strip.empty?
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, INCOMPLETE_SESSION
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def owned_session(access_token:, refresh_token:, user_id:, user: nil)
|
|
59
|
+
Session.new(
|
|
60
|
+
access_token: access_token.dup.freeze,
|
|
61
|
+
refresh_token: refresh_token.dup.freeze,
|
|
62
|
+
user_id: user_id.dup.freeze,
|
|
63
|
+
user: user
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/volcano/auth_sign_up.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Volcano
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def build_sign_up_result(payload)
|
|
32
|
+
raise TypeError, INVALID_SIGN_UP_RESULT unless payload.is_a?(Hash)
|
|
33
|
+
|
|
32
34
|
confirmation_required = payload.fetch('confirmation_required')
|
|
33
35
|
message = payload.fetch('message')
|
|
34
36
|
valid = [true, false].include?(confirmation_required) && message.is_a?(String)
|
|
@@ -4,9 +4,16 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Delivers broadcasts only while their subscription generation is current.
|
|
6
6
|
module BroadcastDelivery
|
|
7
|
-
|
|
7
|
+
# @dynamic emit, with_lifecycle_lock, broadcast?
|
|
8
|
+
# Captures the subscription generation alongside an immutable publication.
|
|
9
|
+
PublicationContext = Data.define(:protocol, :publication, :generation, :recovered)
|
|
10
|
+
|
|
11
|
+
# Freezes the publication before callbacks can observe or modify it.
|
|
12
|
+
class PublicationContext
|
|
13
|
+
# @dynamic protocol, publication, generation, recovered
|
|
8
14
|
def initialize(protocol:, publication:, generation:, recovered:)
|
|
9
|
-
|
|
15
|
+
publication = Immutable.call(publication)
|
|
16
|
+
super
|
|
10
17
|
end
|
|
11
18
|
end
|
|
12
19
|
private_constant :PublicationContext
|
|
@@ -30,6 +37,14 @@ module Volcano
|
|
|
30
37
|
PublicationContext.new(protocol:, publication:, generation:, recovered:)
|
|
31
38
|
end
|
|
32
39
|
|
|
40
|
+
def publication_rejection(protocol, generation)
|
|
41
|
+
return unless broadcast?
|
|
42
|
+
|
|
43
|
+
lambda do |publication|
|
|
44
|
+
reject_broadcast(publication_context(protocol, publication, generation, false))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
33
48
|
def reject_broadcast(context)
|
|
34
49
|
with_current_publication(context) do
|
|
35
50
|
context.protocol.__send__(:drop_publication, @name, context.publication)
|
|
@@ -57,6 +72,15 @@ module Volcano
|
|
|
57
72
|
@publication_protocol.__send__(:delete_recovery_state, @name)
|
|
58
73
|
@publication_protocol = nil
|
|
59
74
|
end
|
|
75
|
+
|
|
76
|
+
def clear_channel_recovery_state
|
|
77
|
+
return unless broadcast?
|
|
78
|
+
|
|
79
|
+
# @type var position: Hash[Symbol, String | Integer]
|
|
80
|
+
position = {}
|
|
81
|
+
@recovery_position = position.freeze
|
|
82
|
+
@recovery_lineage = nil
|
|
83
|
+
end
|
|
60
84
|
end
|
|
61
85
|
private_constant :BroadcastDelivery
|
|
62
86
|
end
|
|
@@ -4,6 +4,9 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Registers channel callbacks and connects them to protocol pushes.
|
|
6
6
|
module ChannelCallbacks
|
|
7
|
+
# @dynamic presence?, postgres?, broadcast?, register_presence_handler, detach_presence_handler
|
|
8
|
+
# @dynamic deliver_broadcast, publication_context, reject_broadcast, publication_rejection
|
|
9
|
+
# @dynamic dispatch_postgres_change
|
|
7
10
|
def on(event, callback = nil, &block)
|
|
8
11
|
raise ArgumentError, "unsupported realtime event: #{event}" unless allowed_event?(event)
|
|
9
12
|
|
|
@@ -37,21 +40,21 @@ module Volcano
|
|
|
37
40
|
generation = @publication_generation
|
|
38
41
|
@publication_protocol = protocol if broadcast?
|
|
39
42
|
@publication_handler = protocol.on_publication(
|
|
40
|
-
@name, on_rejection: publication_rejection(protocol, generation)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
context = publication_context(protocol, publication, generation, recovered)
|
|
45
|
-
deliver_broadcast(event, data, context)
|
|
46
|
-
end
|
|
43
|
+
@name, on_rejection: publication_rejection(protocol, generation),
|
|
44
|
+
&publication_callback(protocol, generation)
|
|
45
|
+
)
|
|
47
46
|
@handler_registered = true
|
|
48
47
|
end
|
|
49
48
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
def publication_callback(protocol, generation)
|
|
50
|
+
lambda do |event, data, publication, recovered:|
|
|
51
|
+
# @type var recovered: bool
|
|
52
|
+
if broadcast?
|
|
53
|
+
context = publication_context(protocol, publication, generation, recovered)
|
|
54
|
+
deliver_broadcast(event, data, context)
|
|
55
|
+
else
|
|
56
|
+
deliver_publication(event, data)
|
|
57
|
+
end
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -67,6 +70,7 @@ module Volcano
|
|
|
67
70
|
callbacks = @callbacks[event].dup
|
|
68
71
|
return if callbacks.empty?
|
|
69
72
|
|
|
73
|
+
# @type var delivery: delivery
|
|
70
74
|
delivery = [event, data, callbacks, before_delivery]
|
|
71
75
|
return defer_callback_delivery(delivery) if callback_dispatching?
|
|
72
76
|
|
|
@@ -109,8 +113,9 @@ module Volcano
|
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
def detach_publication_handler(protocol)
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
handler = @publication_handler
|
|
117
|
+
if handler
|
|
118
|
+
protocol.off_publication(@name, handler)
|
|
114
119
|
@publication_generation += 1 if broadcast?
|
|
115
120
|
end
|
|
116
121
|
detach_presence_handler(protocol)
|
|
@@ -4,6 +4,9 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Coordinates a channel's protocol subscription and local lifecycle state.
|
|
6
6
|
module ChannelLifecycle
|
|
7
|
+
# @dynamic end_postgres_delivery, reset_presence, detach_publication_handler, ensure_open!
|
|
8
|
+
# @dynamic broadcast?, next_presence_epoch, register_handlers, begin_postgres_delivery
|
|
9
|
+
# @dynamic invalidate_presence_subscription, clear_presence, presence?
|
|
7
10
|
def mark_closed
|
|
8
11
|
@closed = true
|
|
9
12
|
@subscription_desired = @subscribed = false
|
|
@@ -84,14 +87,18 @@ module Volcano
|
|
|
84
87
|
return unless broadcast?
|
|
85
88
|
|
|
86
89
|
_, lineage, = @realtime.__send__(:capture_protocol_session)
|
|
87
|
-
|
|
90
|
+
# @type var binding: recovery_binding
|
|
91
|
+
binding = [lineage, recovery_position(lineage)]
|
|
92
|
+
binding.freeze
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
def recovery_position(lineage)
|
|
91
96
|
return @recovery_position if @recovery_lineage == lineage
|
|
92
97
|
|
|
93
98
|
@recovery_lineage = lineage
|
|
94
|
-
@
|
|
99
|
+
# @type var position: recovery_position
|
|
100
|
+
position = {}
|
|
101
|
+
@recovery_position = position.freeze
|
|
95
102
|
end
|
|
96
103
|
|
|
97
104
|
def validate_recovery_binding!(binding)
|
|
@@ -113,13 +120,6 @@ module Volcano
|
|
|
113
120
|
@recovery_position = protocol.__send__(:position, @name) || @recovery_position
|
|
114
121
|
end
|
|
115
122
|
|
|
116
|
-
def clear_channel_recovery_state
|
|
117
|
-
return unless broadcast?
|
|
118
|
-
|
|
119
|
-
@recovery_position = {}.freeze
|
|
120
|
-
@recovery_lineage = nil
|
|
121
|
-
end
|
|
122
|
-
|
|
123
123
|
def mark_removed
|
|
124
124
|
@callbacks.each_value(&:clear)
|
|
125
125
|
@handler_registered = @subscription_desired = @subscribed = false
|
|
@@ -4,6 +4,9 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Opens the WebSocket transport and maps its failures to public errors.
|
|
6
6
|
module Connection
|
|
7
|
+
# @dynamic protocol_connected, protocol_error, protocol_closed, protocol_error_started
|
|
8
|
+
# @dynamic protocol_failed, stop_reconnect
|
|
9
|
+
|
|
7
10
|
private
|
|
8
11
|
|
|
9
12
|
def connect_protocol
|
|
@@ -71,9 +74,9 @@ module Volcano
|
|
|
71
74
|
socket: socket,
|
|
72
75
|
secrets: realtime_secrets,
|
|
73
76
|
events: Protocol::Events.new(
|
|
74
|
-
on_close:
|
|
75
|
-
on_error:
|
|
76
|
-
on_failure:
|
|
77
|
+
on_close: ->(error) { protocol_closed(error) },
|
|
78
|
+
on_error: ->(error) { protocol_error_started(error) },
|
|
79
|
+
on_failure: ->(error, disconnected) { protocol_failed(error, disconnected) }
|
|
77
80
|
)
|
|
78
81
|
)
|
|
79
82
|
end
|
|
@@ -87,7 +90,7 @@ module Volcano
|
|
|
87
90
|
def address
|
|
88
91
|
uri = URI(@api_url)
|
|
89
92
|
uri.scheme = uri.scheme == 'https' ? 'wss' : 'ws'
|
|
90
|
-
uri.path = "#{uri.path.delete_suffix('/')}/realtime/v1/websocket"
|
|
93
|
+
uri.path = "#{uri.path.to_s.delete_suffix('/')}/realtime/v1/websocket"
|
|
91
94
|
encoded_key = URI.encode_www_form_component(@client.anon_token).gsub('+', '%20')
|
|
92
95
|
uri.query = "apikey=#{encoded_key}"
|
|
93
96
|
uri.to_s
|
|
@@ -106,7 +109,8 @@ module Volcano
|
|
|
106
109
|
return redacted unless Transport::NETWORK_ERRORS.any? { |type| error.is_a?(type) }
|
|
107
110
|
|
|
108
111
|
transport_error = Error::TransportError.new(redacted.message)
|
|
109
|
-
|
|
112
|
+
trace = redacted.backtrace
|
|
113
|
+
transport_error.set_backtrace(trace) if trace
|
|
110
114
|
transport_error
|
|
111
115
|
end
|
|
112
116
|
end
|
|
@@ -4,6 +4,7 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Registers and dispatches public connection lifecycle callbacks.
|
|
6
6
|
module ConnectionCallbacks
|
|
7
|
+
# @dynamic start_reconnect
|
|
7
8
|
def on_connect(callback = nil, &block) = register_connection_callback(:connect, callback, block)
|
|
8
9
|
def on_disconnect(callback = nil, &block) = register_connection_callback(:disconnect, callback, block)
|
|
9
10
|
def on_error(callback = nil, &block) = register_connection_callback(:error, callback, block)
|
|
@@ -76,6 +77,7 @@ module Volcano
|
|
|
76
77
|
|
|
77
78
|
def protocol_failed(error, disconnected)
|
|
78
79
|
reset_after_protocol_loss if disconnected
|
|
80
|
+
# @type var events: Array[[connection_event, connection_context]]
|
|
79
81
|
events = [[:error, error_context(error)]]
|
|
80
82
|
events << [:disconnect, disconnect_context(error)] if disconnected
|
|
81
83
|
emit_connection_events(events)
|
|
@@ -84,8 +86,10 @@ module Volcano
|
|
|
84
86
|
|
|
85
87
|
def error_context(error)
|
|
86
88
|
snapshot = immutable_exception(error)
|
|
89
|
+
code = snapshot.respond_to?(:code) ? snapshot.method(:code).call : nil
|
|
90
|
+
code = nil unless code.is_a?(String) || code.is_a?(Integer)
|
|
87
91
|
ErrorContext.new(
|
|
88
|
-
code:
|
|
92
|
+
code: code,
|
|
89
93
|
message: immutable_string(snapshot.message), error: snapshot
|
|
90
94
|
)
|
|
91
95
|
end
|
|
@@ -4,6 +4,7 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Channel collection and connection-state lifecycle operations.
|
|
6
6
|
module Lifecycle
|
|
7
|
+
# @dynamic channel_lock, normalize_channel_type
|
|
7
8
|
def connected? = !@closed && (@protocol&.connected? || false)
|
|
8
9
|
|
|
9
10
|
def remove_channel(name, type: :broadcast)
|
|
@@ -26,7 +27,7 @@ module Volcano
|
|
|
26
27
|
private
|
|
27
28
|
|
|
28
29
|
def remove_channel_locked(name)
|
|
29
|
-
channel = @channels
|
|
30
|
+
channel = @channels.fetch(name, nil)
|
|
30
31
|
return unless channel
|
|
31
32
|
|
|
32
33
|
channel.__send__(:remove)
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Volcano
|
|
4
4
|
class Realtime
|
|
5
|
+
# Immutable limits for one channel's automatic row-fetch queue.
|
|
5
6
|
PostgresBatchConfig = Data.define(
|
|
6
7
|
:auto_fetch, :batch_window_ms, :max_batch_size
|
|
7
|
-
)
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Validates the queue limits supplied to a Postgres channel.
|
|
11
|
+
class PostgresBatchConfig
|
|
12
|
+
# @dynamic auto_fetch, batch_window_ms, max_batch_size
|
|
8
13
|
def initialize(auto_fetch:, batch_window_ms:, max_batch_size:)
|
|
9
14
|
unless batch_window_ms.is_a?(Integer) && batch_window_ms.positive?
|
|
10
15
|
raise ArgumentError, 'fetch_batch_window_ms must be a positive integer'
|
|
@@ -20,6 +25,8 @@ module Volcano
|
|
|
20
25
|
|
|
21
26
|
# Collects compatible row lookups and preserves their callback order.
|
|
22
27
|
module PostgresBatch
|
|
28
|
+
# @dynamic postgres_stop?, current_postgres_request?, expanded_postgres_changes, emit
|
|
29
|
+
|
|
23
30
|
private
|
|
24
31
|
|
|
25
32
|
def collect_postgres_batch(first)
|
|
@@ -35,6 +42,7 @@ module Volcano
|
|
|
35
42
|
while requests.length < @postgres_batch_config.max_batch_size
|
|
36
43
|
request = dequeue_postgres_before(deadline)
|
|
37
44
|
return unless request
|
|
45
|
+
return request unless request.is_a?(PostgresDeliveryRequest)
|
|
38
46
|
return request unless compatible_postgres_request?(requests.first, request)
|
|
39
47
|
|
|
40
48
|
requests << request
|
|
@@ -72,8 +80,8 @@ module Volcano
|
|
|
72
80
|
return unless current_postgres_request?(requests.first)
|
|
73
81
|
|
|
74
82
|
changes = expanded_postgres_changes(requests)
|
|
75
|
-
requests.
|
|
76
|
-
deliver_expanded_postgres_change(request,
|
|
83
|
+
requests.each_with_index do |request, index|
|
|
84
|
+
deliver_expanded_postgres_change(request, changes.fetch(index))
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
@@ -4,23 +4,34 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
PostgresChange = Data.define(
|
|
6
6
|
:type, :schema, :table, :record, :old_record, :columns, :timestamp, :id, :mode
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Immutable snapshot of a Postgres change publication.
|
|
10
|
+
class PostgresChange
|
|
11
|
+
# @dynamic type, schema, table, record, old_record, columns, timestamp, id, mode
|
|
12
|
+
# @dynamic members, with, to_h, deconstruct, deconstruct_keys, self.[], self.members
|
|
13
|
+
def initialize(type:, schema:, table:, timestamp:, **payload)
|
|
14
|
+
type = Immutable.call(type.to_s)
|
|
15
|
+
schema = Immutable.call(schema.to_s)
|
|
16
|
+
table = Immutable.call(table.to_s)
|
|
17
|
+
timestamp = Immutable.call(timestamp.to_s)
|
|
18
|
+
payload = snapshot_payload(payload)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def snapshot_payload(payload)
|
|
25
|
+
payload = { record: nil, old_record: nil, columns: nil, id: nil, mode: nil }.merge(payload)
|
|
26
|
+
%i[record old_record columns id].each { |field| payload[field] = Immutable.optional(payload[field]) }
|
|
27
|
+
payload[:mode] &&= Immutable.call(payload[:mode].to_s)
|
|
28
|
+
payload
|
|
19
29
|
end
|
|
20
30
|
end
|
|
21
31
|
|
|
22
32
|
# Registers filtered callbacks for Postgres change publications.
|
|
23
33
|
module PostgresChanges
|
|
34
|
+
# @dynamic on, enqueue_postgres_delivery
|
|
24
35
|
CHANGE_EVENTS = %w[INSERT UPDATE DELETE].freeze
|
|
25
36
|
EVENTS = [*CHANGE_EVENTS, '*'].freeze
|
|
26
37
|
|
|
@@ -41,7 +52,7 @@ module Volcano
|
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
def postgres_change(data)
|
|
44
|
-
return unless valid_postgres_change?(data)
|
|
55
|
+
return unless data.is_a?(Hash) && valid_postgres_change?(data)
|
|
45
56
|
|
|
46
57
|
PostgresChange.new(
|
|
47
58
|
type: data.fetch('type'), schema: data.fetch('schema'), table: data.fetch('table'),
|
|
@@ -58,7 +69,10 @@ module Volcano
|
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
def register_postgres_listener(event, schema, table, handler)
|
|
61
|
-
|
|
72
|
+
# @type var filtered: ^(Object?) -> void
|
|
73
|
+
filtered = lambda do |change|
|
|
74
|
+
handler.call(change) if change.is_a?(PostgresChange) && change.schema == schema && change.table == table
|
|
75
|
+
end
|
|
62
76
|
@postgres_filters[filtered] = [event, schema, table]
|
|
63
77
|
on(event, filtered)
|
|
64
78
|
end
|
|
@@ -77,7 +91,7 @@ module Volcano
|
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
def valid_postgres_change?(data)
|
|
80
|
-
valid_postgres_identity?(data) && valid_postgres_payload?(data)
|
|
94
|
+
data.is_a?(Hash) && valid_postgres_identity?(data) && valid_postgres_payload?(data)
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
def valid_postgres_identity?(data)
|
|
@@ -87,8 +101,9 @@ module Volcano
|
|
|
87
101
|
end
|
|
88
102
|
|
|
89
103
|
def valid_postgres_payload?(data)
|
|
104
|
+
mode = data['mode']
|
|
90
105
|
optional_hash?(data['record']) && optional_hash?(data['old_record']) &&
|
|
91
|
-
optional_array?(data['columns']) &&
|
|
106
|
+
optional_array?(data['columns']) && (mode.nil? || mode == 'lightweight')
|
|
92
107
|
end
|
|
93
108
|
|
|
94
109
|
def optional_hash?(value) = value.nil? || value.is_a?(Hash)
|
|
@@ -10,6 +10,8 @@ module Volcano
|
|
|
10
10
|
|
|
11
11
|
attr_reader :database_name
|
|
12
12
|
|
|
13
|
+
# @dynamic database_name
|
|
14
|
+
|
|
13
15
|
def database_name=(name)
|
|
14
16
|
unless valid_database_name?(name)
|
|
15
17
|
raise ArgumentError,
|
|
@@ -48,12 +50,23 @@ module Volcano
|
|
|
48
50
|
|
|
49
51
|
def fetch_postgres_rows(change, ids, database_name, access_token)
|
|
50
52
|
table = change.schema == 'public' ? change.table : "#{change.schema}.#{change.table}"
|
|
51
|
-
postgres_fetch_semaphore.acquire do
|
|
53
|
+
rows = postgres_fetch_semaphore.acquire do
|
|
52
54
|
BlockingCall.call do
|
|
53
55
|
@client.__send__(:database_with_token, database_name, access_token)
|
|
54
56
|
.from(table).select('*').in('id', ids).execute
|
|
55
57
|
end
|
|
56
58
|
end
|
|
59
|
+
validate_postgres_rows(rows)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_postgres_rows(rows)
|
|
63
|
+
raise TypeError, 'Postgres row lookup must return an array' unless rows.is_a?(Array)
|
|
64
|
+
|
|
65
|
+
rows.map do |row|
|
|
66
|
+
raise TypeError, 'Postgres row lookup must return objects' unless row.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
row
|
|
69
|
+
end
|
|
57
70
|
end
|
|
58
71
|
|
|
59
72
|
def postgres_fetch_semaphore
|
|
@@ -9,6 +9,7 @@ module Volcano
|
|
|
9
9
|
|
|
10
10
|
# Serializes Postgres publications and expands lightweight row identities.
|
|
11
11
|
module PostgresDelivery
|
|
12
|
+
# @dynamic postgres?, collect_postgres_batch, deliver_postgres_batch
|
|
12
13
|
QUEUE_LIMIT = 128
|
|
13
14
|
STOP = Object.new.freeze
|
|
14
15
|
private_constant :QUEUE_LIMIT, :STOP
|
|
@@ -18,7 +19,9 @@ module Volcano
|
|
|
18
19
|
def initialize_postgres_delivery(batch_config)
|
|
19
20
|
require 'async/queue'
|
|
20
21
|
@postgres_batch_config = batch_config
|
|
21
|
-
@
|
|
22
|
+
# @type var empty_filters: Hash[^(Object?) -> void, [String, String, String]]
|
|
23
|
+
empty_filters = {}
|
|
24
|
+
@postgres_filters = empty_filters
|
|
22
25
|
@postgres_queue = Async::LimitedQueue.new(QUEUE_LIMIT)
|
|
23
26
|
@postgres_worker = nil
|
|
24
27
|
@postgres_epoch = 0
|
|
@@ -88,10 +91,12 @@ module Volcano
|
|
|
88
91
|
end
|
|
89
92
|
|
|
90
93
|
def run_postgres_worker
|
|
94
|
+
# @type var pending: Object?
|
|
91
95
|
pending = nil
|
|
92
96
|
loop do
|
|
93
97
|
request = pending || @postgres_queue.dequeue
|
|
94
98
|
break if postgres_stop?(request)
|
|
99
|
+
raise TypeError, 'invalid Postgres delivery request' unless request.is_a?(PostgresDeliveryRequest)
|
|
95
100
|
|
|
96
101
|
requests, pending = collect_postgres_batch(request)
|
|
97
102
|
deliver_postgres_batch(requests)
|
|
@@ -4,6 +4,8 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Expands lightweight Postgres identities into callback-ready changes.
|
|
6
6
|
module PostgresExpansion
|
|
7
|
+
# @dynamic current_postgres_request?, postgres_batch_key
|
|
8
|
+
|
|
7
9
|
private
|
|
8
10
|
|
|
9
11
|
def expanded_postgres_changes(requests)
|
|
@@ -26,12 +28,16 @@ module Volcano
|
|
|
26
28
|
|
|
27
29
|
def fetch_postgres_records(requests)
|
|
28
30
|
request = requests.first
|
|
31
|
+
database_name = request.database_name
|
|
32
|
+
lineage = request.session_lineage
|
|
33
|
+
raise TypeError, 'invalid Postgres fetch request' unless database_name && lineage.is_a?(SessionOperations)
|
|
34
|
+
|
|
29
35
|
access_token = @realtime.__send__(
|
|
30
|
-
:access_token_for_protocol_lineage,
|
|
36
|
+
:access_token_for_protocol_lineage, lineage
|
|
31
37
|
)
|
|
32
38
|
rows = @realtime.__send__(
|
|
33
39
|
:fetch_postgres_rows, request.change, postgres_ids(requests),
|
|
34
|
-
|
|
40
|
+
database_name, access_token
|
|
35
41
|
)
|
|
36
42
|
rows.to_h { |row| [row.fetch('id').to_s, row] }
|
|
37
43
|
end
|
|
@@ -41,7 +47,7 @@ module Volcano
|
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def expanded_postgres_record(request, records)
|
|
44
|
-
record = records
|
|
50
|
+
record = records.fetch(request.change.id.to_s, nil)
|
|
45
51
|
unless record
|
|
46
52
|
report_postgres_fetch_error(request, Error::NotFoundError.new('Postgres row not found'))
|
|
47
53
|
return request.change
|
|
@@ -55,7 +61,7 @@ module Volcano
|
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
def delete_postgres_change(change)
|
|
58
|
-
old_record = change.old_record || (change.id
|
|
64
|
+
old_record = change.old_record || (change.id ? { 'id' => change.id } : nil)
|
|
59
65
|
change.with(old_record:, id: nil, mode: nil)
|
|
60
66
|
end
|
|
61
67
|
|