volcano-sdk 0.31.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/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/version.rb +1 -1
- data/sig/realtime.rbs +46 -0
- data/sig/realtime_postgres_change.rbs +1 -2
- metadata +2 -1
|
@@ -14,6 +14,8 @@ module Volcano
|
|
|
14
14
|
class ServerError < StandardError
|
|
15
15
|
attr_reader :code
|
|
16
16
|
|
|
17
|
+
# @dynamic code
|
|
18
|
+
|
|
17
19
|
def initialize(message, code: nil)
|
|
18
20
|
super(message)
|
|
19
21
|
@code = code.is_a?(String) ? code.dup.freeze : code
|
|
@@ -46,12 +48,10 @@ module Volcano
|
|
|
46
48
|
def self.connect(id:, token:) = { 'id' => id, 'connect' => { 'token' => token } }
|
|
47
49
|
|
|
48
50
|
def self.subscribe(id:, channel:, recoverable: false, join_leave: false, recovery: nil)
|
|
51
|
+
# @type var options: Hash[String, Object]
|
|
49
52
|
options = { 'channel' => channel }
|
|
50
|
-
if recovery
|
|
51
|
-
|
|
52
|
-
elsif recoverable
|
|
53
|
-
options['recoverable'] = true
|
|
54
|
-
end
|
|
53
|
+
add_recovery_options(options, recovery) if recovery
|
|
54
|
+
options['recoverable'] = true if !recovery && recoverable
|
|
55
55
|
options['join_leave'] = true if join_leave
|
|
56
56
|
{ 'id' => id, 'subscribe' => options }
|
|
57
57
|
end
|
|
@@ -88,8 +88,9 @@ module Volcano
|
|
|
88
88
|
def connect(token:)
|
|
89
89
|
result = request('connect') { |id| self.class.connect(id: id, token: token) }
|
|
90
90
|
ensure_open!
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
raise TypeError, 'realtime connect result must be an object' unless result.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
result.tap { @connected = true }
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def subscribe(channel:, recoverable: false, join_leave: false, recovery: nil)
|
|
@@ -111,7 +112,11 @@ module Volcano
|
|
|
111
112
|
def unsubscribe(channel:)
|
|
112
113
|
@subscription_lock.acquire do
|
|
113
114
|
ensure_open!
|
|
114
|
-
|
|
115
|
+
unless @subscriptions.include?(channel)
|
|
116
|
+
# @type var empty_reply: Hash[String, Object]
|
|
117
|
+
empty_reply = {}
|
|
118
|
+
next empty_reply
|
|
119
|
+
end
|
|
115
120
|
|
|
116
121
|
result = request('unsubscribe') { |id| self.class.unsubscribe(id: id, channel: channel) }
|
|
117
122
|
@subscriptions.delete(channel)
|
|
@@ -146,7 +151,7 @@ module Volcano
|
|
|
146
151
|
end
|
|
147
152
|
|
|
148
153
|
def ensure_open!
|
|
149
|
-
raise
|
|
154
|
+
raise(@closed_error || ClosedError.new('realtime connection closed')) if @closed
|
|
150
155
|
|
|
151
156
|
self
|
|
152
157
|
end
|
|
@@ -4,6 +4,9 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Dispatches protocol replies and publications to their waiting consumers.
|
|
6
6
|
module ProtocolDispatch
|
|
7
|
+
# @dynamic write_frame, dispatch_publication, dispatch_queued_callback, close_with, closed_error
|
|
8
|
+
PresenceDelivery = Data.define(:handlers, :event, :data)
|
|
9
|
+
|
|
7
10
|
private
|
|
8
11
|
|
|
9
12
|
def process(frame)
|
|
@@ -12,7 +15,10 @@ module Volcano
|
|
|
12
15
|
elsif frame.key?('id')
|
|
13
16
|
dispatch_reply(frame)
|
|
14
17
|
elsif frame.key?('push')
|
|
15
|
-
|
|
18
|
+
push = frame.fetch('push')
|
|
19
|
+
raise TypeError, 'realtime push must be an object' unless push.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
dispatch_push(push)
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
|
|
@@ -27,7 +33,10 @@ module Volcano
|
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def dispatch_reply(frame)
|
|
30
|
-
|
|
36
|
+
id = frame.fetch('id')
|
|
37
|
+
return unless id.is_a?(Integer)
|
|
38
|
+
|
|
39
|
+
pending = @pending.fetch(id, nil)
|
|
31
40
|
return unless pending
|
|
32
41
|
|
|
33
42
|
reply = reply_value(frame, pending.reply_key)
|
|
@@ -45,12 +54,18 @@ module Volcano
|
|
|
45
54
|
|
|
46
55
|
def reply_value(frame, reply_key)
|
|
47
56
|
error = frame['error']
|
|
48
|
-
return
|
|
57
|
+
return successful_reply(frame, reply_key) unless error
|
|
58
|
+
|
|
59
|
+
raise TypeError, 'realtime error must be an object' unless error.is_a?(Hash)
|
|
49
60
|
|
|
50
61
|
message = error['message'] || 'realtime command failed'
|
|
51
62
|
Protocol::Failure.new(error: ServerError.new(message, code: error['code']))
|
|
52
63
|
end
|
|
53
64
|
|
|
65
|
+
def successful_reply(frame, reply_key)
|
|
66
|
+
frame.fetch(reply_key) { frame.fetch('result') { frame.except('id') } }
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
def dispatch_presence(push, event)
|
|
55
70
|
channel = matching_channel(@presence_handlers, push['channel'].to_s)
|
|
56
71
|
info = push.dig(event, 'info')
|
|
@@ -61,7 +76,8 @@ module Volcano
|
|
|
61
76
|
return
|
|
62
77
|
end
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
delivery = PresenceDelivery.new(handlers: @presence_handlers.fetch(channel).dup, event:, data: info)
|
|
80
|
+
@callback_queue.enqueue(delivery)
|
|
65
81
|
end
|
|
66
82
|
|
|
67
83
|
def matching_channel(handlers, channel)
|
|
@@ -93,7 +109,7 @@ module Volcano
|
|
|
93
109
|
|
|
94
110
|
def dispatch_callback_delivery(handlers, event, data)
|
|
95
111
|
handlers.each do |handler|
|
|
96
|
-
break if @callback_stopping
|
|
112
|
+
break handlers if @callback_stopping
|
|
97
113
|
|
|
98
114
|
handler.call(event, data)
|
|
99
115
|
rescue StandardError
|
|
@@ -105,9 +121,11 @@ module Volcano
|
|
|
105
121
|
return if @callback_queue.size >= @max_callback_queue
|
|
106
122
|
|
|
107
123
|
channel, handlers = @pending_presence_resyncs.shift
|
|
108
|
-
return unless channel
|
|
124
|
+
return unless channel && handlers
|
|
109
125
|
|
|
110
|
-
@
|
|
126
|
+
# @type var empty_info: Hash[String, Object?]
|
|
127
|
+
empty_info = {}
|
|
128
|
+
@callback_queue.enqueue(PresenceDelivery.new(handlers:, event: 'sync_required', data: empty_info))
|
|
111
129
|
end
|
|
112
130
|
end
|
|
113
131
|
end
|
|
@@ -6,12 +6,15 @@ module Volcano
|
|
|
6
6
|
class Protocol
|
|
7
7
|
# Coordinates protocol requests and the socket reader loop.
|
|
8
8
|
module IO
|
|
9
|
+
# @dynamic ensure_open!, close_with, process
|
|
10
|
+
|
|
9
11
|
private
|
|
10
12
|
|
|
11
13
|
def request(reply_key, on_reply: nil)
|
|
14
|
+
# @type var registered_id: Integer?
|
|
12
15
|
ensure_open!
|
|
13
|
-
|
|
14
|
-
write_frame(yield(id))
|
|
16
|
+
registered_id, reply_queue = register_request(reply_key, on_reply)
|
|
17
|
+
write_frame(yield(registered_id || raise(TypeError, 'invalid realtime request id')))
|
|
15
18
|
reply = await_reply(reply_queue)
|
|
16
19
|
raise reply.error if reply.is_a?(Failure)
|
|
17
20
|
|
|
@@ -19,7 +22,7 @@ module Volcano
|
|
|
19
22
|
rescue StandardError => e
|
|
20
23
|
raise Redaction.exception(e, secrets: @secrets), cause: nil
|
|
21
24
|
ensure
|
|
22
|
-
@pending.delete(
|
|
25
|
+
@pending.delete(registered_id) if registered_id
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def register_request(reply_key, on_reply)
|
|
@@ -83,7 +86,8 @@ module Volcano
|
|
|
83
86
|
|
|
84
87
|
def closed_error(error)
|
|
85
88
|
ClosedError.new(Redaction.message(error.message, secrets: @secrets)).tap do |closed|
|
|
86
|
-
|
|
89
|
+
trace = error.backtrace
|
|
90
|
+
closed.set_backtrace(trace) if trace
|
|
87
91
|
end
|
|
88
92
|
end
|
|
89
93
|
end
|
|
@@ -5,6 +5,8 @@ module Volcano
|
|
|
5
5
|
class Protocol
|
|
6
6
|
# Exposes connection state and manages publication handlers.
|
|
7
7
|
module Lifecycle
|
|
8
|
+
# @dynamic ensure_open!, initialize_recovery_positions, initialize_publication_producer, dispatch_callbacks
|
|
9
|
+
# @dynamic dispatch_publication_batches, read_loop
|
|
8
10
|
def connected? = @connected && !@closed
|
|
9
11
|
|
|
10
12
|
def on_publication(channel, on_rejection: nil, &handler)
|
|
@@ -4,6 +4,8 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Normalizes publications and admits live deliveries without blocking the reader.
|
|
6
6
|
module ProtocolPublicationDispatch
|
|
7
|
+
# @dynamic enqueue_live_publication, matching_channel, drop_publication, mark_unknown_gap
|
|
8
|
+
# @dynamic dispatch_callback_delivery
|
|
7
9
|
Registration = Data.define(:handler, :on_rejection)
|
|
8
10
|
PublicationDelivery = Data.define(
|
|
9
11
|
:channel, :handlers, :event, :data, :publication, :recovered, :on_rejection, :rejection_key
|
|
@@ -24,12 +26,15 @@ module Volcano
|
|
|
24
26
|
def publication_delivery(channel, publication, recovered:)
|
|
25
27
|
matched_channel = matching_publication_channel(channel)
|
|
26
28
|
return unless matched_channel
|
|
27
|
-
return reject_invalid_publication(matched_channel, publication) unless
|
|
29
|
+
return reject_invalid_publication(matched_channel, publication) unless publication.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
data = publication['data']
|
|
32
|
+
return reject_invalid_publication(matched_channel, publication) unless data.is_a?(Hash)
|
|
28
33
|
unless @publication_handlers.key?(matched_channel)
|
|
29
34
|
return drop_handlerless_publication(matched_channel, publication)
|
|
30
35
|
end
|
|
31
36
|
|
|
32
|
-
build_publication_delivery(matched_channel, publication, recovered: recovered)
|
|
37
|
+
build_publication_delivery(matched_channel, publication, data, recovered: recovered)
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def matching_publication_channel(channel)
|
|
@@ -38,10 +43,6 @@ module Volcano
|
|
|
38
43
|
[handler_channel, recovery_channel].compact.max_by(&:length)
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
def valid_publication?(publication)
|
|
42
|
-
publication.is_a?(Hash) && publication['data'].is_a?(Hash)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
46
|
def reject_invalid_publication(channel, publication)
|
|
46
47
|
if publication.is_a?(Hash)
|
|
47
48
|
drop_publication(channel, publication)
|
|
@@ -56,9 +57,8 @@ module Volcano
|
|
|
56
57
|
nil
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
def build_publication_delivery(channel, publication, recovered:)
|
|
60
|
+
def build_publication_delivery(channel, publication, data, recovered:)
|
|
60
61
|
registrations = @publication_handlers.fetch(channel).dup.freeze
|
|
61
|
-
data = publication.fetch('data')
|
|
62
62
|
PublicationDelivery.new(
|
|
63
63
|
channel: channel.dup.freeze,
|
|
64
64
|
handlers: registrations.map(&:handler).freeze,
|
|
@@ -98,14 +98,16 @@ module Volcano
|
|
|
98
98
|
def dispatch_queued_callback(delivery)
|
|
99
99
|
if delivery.is_a?(PublicationDelivery)
|
|
100
100
|
dispatch_publication_callbacks(delivery)
|
|
101
|
+
elsif delivery.is_a?(ProtocolDispatch::PresenceDelivery)
|
|
102
|
+
dispatch_callback_delivery(delivery.handlers, delivery.event, delivery.data)
|
|
101
103
|
else
|
|
102
|
-
|
|
104
|
+
raise TypeError, 'invalid realtime callback delivery'
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
|
|
106
108
|
def dispatch_publication_callbacks(delivery)
|
|
107
109
|
delivery.handlers.each do |handler|
|
|
108
|
-
break if @callback_stopping
|
|
110
|
+
break delivery.handlers if @callback_stopping
|
|
109
111
|
|
|
110
112
|
handler.call(
|
|
111
113
|
delivery.event, delivery.data, delivery.publication, recovered: delivery.recovered
|
|
@@ -4,6 +4,10 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Validates recovery subscribe results before updating stream positions.
|
|
6
6
|
module ProtocolRecovery
|
|
7
|
+
# @dynamic class, request, dispatch_recovered_publications, recovery_position
|
|
8
|
+
# @dynamic delete_recovery_state, set_recovery_position, mark_unknown_gap
|
|
9
|
+
# @dynamic mark_publication_gap, same_epoch?
|
|
10
|
+
|
|
7
11
|
private
|
|
8
12
|
|
|
9
13
|
def subscribe_request(channel:, recoverable:, join_leave:, recovery:)
|
|
@@ -28,7 +32,7 @@ module Volcano
|
|
|
28
32
|
|
|
29
33
|
def parse_recovery_result(channel:, recovery:, result:)
|
|
30
34
|
result_position, publications = validated_recovery_result(result)
|
|
31
|
-
return block_malformed_recovery_result(channel, recovery) unless result_position
|
|
35
|
+
return block_malformed_recovery_result(channel, recovery) unless result_position && publications
|
|
32
36
|
|
|
33
37
|
requested_position = requested_recovery_position(recovery)
|
|
34
38
|
return block_recovery_result(channel, requested_position) if unrecovered_empty_result?(
|
|
@@ -50,7 +54,7 @@ module Volcano
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def unrecovered_empty_result?(result, requested_position, publications)
|
|
53
|
-
requested_position && result['recovered'] == false && publications.empty?
|
|
57
|
+
requested_position && result.is_a?(Hash) && result['recovered'] == false && publications.empty?
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
def validated_recovery_result(result)
|
|
@@ -68,7 +72,7 @@ module Volcano
|
|
|
68
72
|
validated = publications.map { |publication| validated_recovery_publication(publication, result_position) }
|
|
69
73
|
return [nil, nil] if validated.any?(&:nil?)
|
|
70
74
|
|
|
71
|
-
[result_position, validated]
|
|
75
|
+
[result_position, validated.compact]
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def validated_recovery_publication(publication, fallback_position)
|
|
@@ -4,6 +4,8 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Preserves publication order while recovered deliveries wait for callback capacity.
|
|
6
6
|
module ProtocolRecoveryDispatch
|
|
7
|
+
# @dynamic publication_delivery, reject_publication_delivery, admit_live_publication
|
|
8
|
+
# @dynamic close_with, closed_error
|
|
7
9
|
PublicationBatch = Data.define(:channel, :deliveries, :rejection_key)
|
|
8
10
|
private_constant :PublicationBatch
|
|
9
11
|
|
|
@@ -4,6 +4,7 @@ module Volcano
|
|
|
4
4
|
class Realtime
|
|
5
5
|
# Reconnects unexpected transport losses and restores subscription intent.
|
|
6
6
|
module Reconnect
|
|
7
|
+
# @dynamic channel_lock, protocol_lock, connect_protocol, report_channel_error
|
|
7
8
|
RECONNECT_MIN_DELAY = 0.1
|
|
8
9
|
RECONNECT_MAX_DELAY = 20.0
|
|
9
10
|
MAX_BACKOFF_EXPONENT = 8
|
|
@@ -19,7 +20,9 @@ module Volcano
|
|
|
19
20
|
def stop_reconnect
|
|
20
21
|
task = @reconnect_task
|
|
21
22
|
@reconnect_task = nil
|
|
22
|
-
|
|
23
|
+
return unless task
|
|
24
|
+
|
|
25
|
+
task.stop unless task.equal?(Async::Task.current)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def reconnect_loop
|
data/lib/volcano/realtime.rb
CHANGED
|
@@ -32,134 +32,148 @@ module Volcano
|
|
|
32
32
|
|
|
33
33
|
# Recursively snapshots JSON-compatible values for callback and state safety.
|
|
34
34
|
module Immutable
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def optional(value)
|
|
38
|
-
value && call(value)
|
|
35
|
+
def self.optional(value)
|
|
36
|
+
value && snapshot(value)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
def call(value)
|
|
39
|
+
def self.call(value) = snapshot(value)
|
|
40
|
+
|
|
41
|
+
def self.snapshot(value)
|
|
42
42
|
case value
|
|
43
43
|
when Hash then call_hash(value)
|
|
44
|
-
when Array then value.map { |child|
|
|
44
|
+
when Array then value.map { |child| snapshot(child) }.freeze
|
|
45
45
|
when String then value.dup.freeze
|
|
46
46
|
else value.freeze
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def call_hash(value)
|
|
51
|
-
value.to_h { |key, item| [
|
|
50
|
+
def self.call_hash(value)
|
|
51
|
+
value.to_h { |key, item| [snapshot(key), snapshot(item)] }.freeze
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
private_constant :Immutable
|
|
55
55
|
|
|
56
|
-
PresenceInfo = Data.define(:client, :user, :data)
|
|
56
|
+
PresenceInfo = Data.define(:client, :user, :data)
|
|
57
|
+
|
|
58
|
+
# An immutable snapshot of one presence client.
|
|
59
|
+
class PresenceInfo
|
|
60
|
+
# @dynamic client, user, data, members, with, to_h, deconstruct, deconstruct_keys
|
|
61
|
+
# @dynamic self.[], self.members
|
|
57
62
|
def initialize(client:, user: nil, data: {})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
+
client = Immutable.call(client.to_s)
|
|
64
|
+
user = user.nil? ? nil : Immutable.call(user.to_s)
|
|
65
|
+
data = Immutable.call(data)
|
|
66
|
+
super
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
CHANNEL_TYPES = %i[broadcast presence postgres].freeze
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
# Owns the public realtime facade and its channel collection.
|
|
73
|
+
module Core
|
|
74
|
+
# @dynamic initialize_postgres_database, connect_protocol, close_protocol, public_error
|
|
75
|
+
# @dynamic protocol_error, open_socket, default_reconnect_delay, __send__
|
|
76
|
+
def initialize(client, api_url:, socket_factory: nil, reconnect_delay: nil)
|
|
77
|
+
@client = client
|
|
78
|
+
@api_url = api_url
|
|
79
|
+
@socket_factory = socket_factory || ->(address) { open_socket(address) }
|
|
80
|
+
@reconnect_delay = reconnect_delay || ->(attempt) { default_reconnect_delay(attempt) }
|
|
81
|
+
initialize_realtime_state
|
|
82
|
+
initialize_postgres_database
|
|
83
|
+
end
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
type = normalize_channel_type(type)
|
|
95
|
-
batch_config = PostgresBatchConfig.new(
|
|
96
|
-
auto_fetch:, batch_window_ms: fetch_batch_window_ms,
|
|
97
|
-
max_batch_size: fetch_max_batch_size
|
|
85
|
+
def initialize_realtime_state
|
|
86
|
+
@protocol = nil
|
|
87
|
+
@protocol_lock = nil
|
|
88
|
+
@channel_lock = nil
|
|
89
|
+
@channels = {}
|
|
90
|
+
@connection_callbacks = { connect: {}, disconnect: {}, error: {} }
|
|
91
|
+
@next_connection_callback_id = 0
|
|
92
|
+
@protocol_session_lineage = nil
|
|
93
|
+
@reconnect_task = nil
|
|
94
|
+
@closed = false
|
|
95
|
+
end
|
|
96
|
+
private :initialize_realtime_state
|
|
97
|
+
|
|
98
|
+
def channel(
|
|
99
|
+
name, type: :broadcast, auto_fetch: true,
|
|
100
|
+
fetch_batch_window_ms: 20, fetch_max_batch_size: 50
|
|
98
101
|
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
type = normalize_channel_type(type)
|
|
103
|
+
batch_config = PostgresBatchConfig.new(
|
|
104
|
+
auto_fetch:, batch_window_ms: fetch_batch_window_ms,
|
|
105
|
+
max_batch_size: fetch_max_batch_size
|
|
106
|
+
)
|
|
107
|
+
channel_lock.acquire do
|
|
108
|
+
ensure_open!
|
|
109
|
+
channel_name = "#{type}:#{name}"
|
|
110
|
+
channel = @channels[channel_name]
|
|
111
|
+
next cached_channel(channel, batch_config) if channel
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
@channels[channel_name] = build_channel(channel_name, type, batch_config)
|
|
114
|
+
end
|
|
106
115
|
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def protocol
|
|
110
|
-
ensure_open!
|
|
111
|
-
return @protocol if @protocol
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
def protocol
|
|
114
118
|
ensure_open!
|
|
115
|
-
@protocol
|
|
119
|
+
current_protocol = @protocol
|
|
120
|
+
return current_protocol if current_protocol
|
|
121
|
+
|
|
122
|
+
protocol_lock.acquire do
|
|
123
|
+
ensure_open!
|
|
124
|
+
@protocol ||= connect_protocol
|
|
125
|
+
end
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
raise public_error(e), cause: nil
|
|
116
128
|
end
|
|
117
|
-
|
|
118
|
-
raise public_error(e), cause: nil
|
|
119
|
-
end
|
|
120
|
-
private :protocol
|
|
129
|
+
private :protocol
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
def disconnect
|
|
132
|
+
lock = @protocol_lock
|
|
133
|
+
lock ? lock.acquire { close_protocol } : close_protocol
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
raise public_error(e), cause: nil
|
|
136
|
+
end
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
def ensure_open!
|
|
139
|
+
raise ClosedError, 'realtime connection closed' if @closed
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
self
|
|
142
|
+
end
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
private
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
def normalize_channel_type(type)
|
|
147
|
+
normalized = type.to_s.to_sym
|
|
148
|
+
return normalized if CHANNEL_TYPES.include?(normalized)
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
raise ArgumentError, "unsupported realtime channel type: #{type}"
|
|
151
|
+
end
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
def report_channel_error(error) = protocol_error(public_error(error))
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
def cached_channel(channel, batch_config)
|
|
156
|
+
channel.__send__(:ensure_fetch_config!, batch_config)
|
|
157
|
+
channel
|
|
158
|
+
end
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
def build_channel(name, type, batch_config)
|
|
161
|
+
Channel.new(self, -> { protocol }, name, type, batch_config:)
|
|
162
|
+
end
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
def protocol_lock
|
|
165
|
+
require 'async/semaphore'
|
|
166
|
+
@protocol_lock ||= Async::Semaphore.new(1)
|
|
167
|
+
end
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
def channel_lock
|
|
170
|
+
require 'async/semaphore'
|
|
171
|
+
@channel_lock ||= Async::Semaphore.new(1)
|
|
172
|
+
end
|
|
162
173
|
end
|
|
174
|
+
include Core
|
|
175
|
+
|
|
176
|
+
private_constant :Core
|
|
163
177
|
|
|
164
178
|
# Represents one realtime broadcast channel.
|
|
165
179
|
class Channel
|
|
@@ -175,14 +189,15 @@ module Volcano
|
|
|
175
189
|
|
|
176
190
|
attr_reader :name
|
|
177
191
|
|
|
192
|
+
# @dynamic name
|
|
193
|
+
|
|
178
194
|
def initialize(realtime, protocol_provider, name, type, batch_config:)
|
|
179
195
|
@realtime = realtime
|
|
180
196
|
@protocol_provider = protocol_provider
|
|
181
197
|
@name = name.freeze
|
|
182
198
|
@handler_registered = @subscribed = @subscription_desired = @closed = false
|
|
183
199
|
@lifecycle_lock = nil
|
|
184
|
-
|
|
185
|
-
@recovery_lineage = nil
|
|
200
|
+
initialize_recovery_state
|
|
186
201
|
initialize_callback_dispatch
|
|
187
202
|
initialize_presence(type)
|
|
188
203
|
initialize_postgres_delivery(batch_config)
|
|
@@ -208,7 +223,7 @@ module Volcano
|
|
|
208
223
|
|
|
209
224
|
def unsubscribe
|
|
210
225
|
state = with_lifecycle_lock { unsubscribe_protocol }
|
|
211
|
-
emit_presence_sync(state)
|
|
226
|
+
emit_presence_sync(state) if state.is_a?(Hash)
|
|
212
227
|
nil
|
|
213
228
|
end
|
|
214
229
|
|
|
@@ -218,11 +233,12 @@ module Volcano
|
|
|
218
233
|
|
|
219
234
|
def restore_subscription(protocol)
|
|
220
235
|
state = with_lifecycle_lock do
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
restoration_needed? ? subscribe_protocol(protocol) : nil
|
|
237
|
+
end
|
|
238
|
+
if state
|
|
239
|
+
subscribed_protocol, epoch = state
|
|
240
|
+
sync_presence(subscribed_protocol, epoch)
|
|
224
241
|
end
|
|
225
|
-
sync_presence(*state) if state
|
|
226
242
|
nil
|
|
227
243
|
end
|
|
228
244
|
|
|
@@ -240,6 +256,13 @@ module Volcano
|
|
|
240
256
|
|
|
241
257
|
private
|
|
242
258
|
|
|
259
|
+
def initialize_recovery_state
|
|
260
|
+
# @type var recovery_position: Hash[Symbol, String | Integer]
|
|
261
|
+
recovery_position = {}
|
|
262
|
+
@recovery_position = recovery_position.freeze
|
|
263
|
+
@recovery_lineage = nil
|
|
264
|
+
end
|
|
265
|
+
|
|
243
266
|
def subscribe_with_intent
|
|
244
267
|
ensure_open!
|
|
245
268
|
raise DuplicateSubscriptionError, "already subscribed to #{@name}" if @subscription_desired
|
|
@@ -254,8 +277,8 @@ module Volcano
|
|
|
254
277
|
|
|
255
278
|
def with_lifecycle_lock(&)
|
|
256
279
|
require 'async/semaphore'
|
|
257
|
-
@lifecycle_lock ||= Async::Semaphore.new(1)
|
|
258
|
-
|
|
280
|
+
lock = @lifecycle_lock ||= Async::Semaphore.new(1)
|
|
281
|
+
lock.acquire(&)
|
|
259
282
|
end
|
|
260
283
|
|
|
261
284
|
def ensure_open!
|
data/lib/volcano/version.rb
CHANGED