smplkit 1.0.8 → 1.0.10
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/smplkit/_generated/app/lib/smplkit_app_client/api/auth_api.rb +3 -0
- data/lib/smplkit/_generated/app/lib/smplkit_app_client/models/account.rb +27 -5
- data/lib/smplkit/_generated/app/lib/smplkit_app_client/models/register_request.rb +49 -4
- data/lib/smplkit/_generated/app/spec/api/auth_api_spec.rb +1 -0
- data/lib/smplkit/_generated/app/spec/models/account_spec.rb +12 -0
- data/lib/smplkit/_generated/app/spec/models/register_request_spec.rb +10 -0
- data/lib/smplkit/ws.rb +193 -30
- metadata +43 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42f9ef066eeb2612d4c6c4cc042933337e10562047eb41400506466a47ec4e99
|
|
4
|
+
data.tar.gz: 69d8819176f26b5199f2c508a71d77397106bfabe0ea5b99b071a3e83199dd0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f05531a8b81d3aa42895ed48f8e7b1680d9cf9a490c356b5ce4cf311b304d862964cb49e18b72e7e982ce1e351b2f7be2234c0be06be6c58cae3d3566c37c53
|
|
7
|
+
data.tar.gz: f45d376af397dad147c4ae24100bdfd990766e511ce4a2de10539e3d358375098c413bf46f09c7f8e149d0333aa3c5efdca88baaebdd275ebda03490fff5370c
|
|
@@ -25,6 +25,7 @@ module SmplkitGeneratedClient::App
|
|
|
25
25
|
# @param [Hash] opts the optional parameters
|
|
26
26
|
# @option opts [String] :mode (default to 'signin')
|
|
27
27
|
# @option opts [String] :source
|
|
28
|
+
# @option opts [String] :entry_point
|
|
28
29
|
# @return [nil]
|
|
29
30
|
def begin_oidc_login(provider, opts = {})
|
|
30
31
|
begin_oidc_login_with_http_info(provider, opts)
|
|
@@ -37,6 +38,7 @@ module SmplkitGeneratedClient::App
|
|
|
37
38
|
# @param [Hash] opts the optional parameters
|
|
38
39
|
# @option opts [String] :mode (default to 'signin')
|
|
39
40
|
# @option opts [String] :source
|
|
41
|
+
# @option opts [String] :entry_point
|
|
40
42
|
# @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
|
|
41
43
|
def begin_oidc_login_with_http_info(provider, opts = {})
|
|
42
44
|
if @api_client.config.debugging
|
|
@@ -53,6 +55,7 @@ module SmplkitGeneratedClient::App
|
|
|
53
55
|
query_params = opts[:query_params] || {}
|
|
54
56
|
query_params[:'mode'] = opts[:'mode'] if !opts[:'mode'].nil?
|
|
55
57
|
query_params[:'source'] = opts[:'source'] if !opts[:'source'].nil?
|
|
58
|
+
query_params[:'entry_point'] = opts[:'entry_point'] if !opts[:'entry_point'].nil?
|
|
56
59
|
|
|
57
60
|
# header parameters
|
|
58
61
|
header_params = opts[:header_params] || {}
|
|
@@ -29,6 +29,12 @@ module SmplkitGeneratedClient::App
|
|
|
29
29
|
|
|
30
30
|
attr_accessor :product_subscriptions
|
|
31
31
|
|
|
32
|
+
# Registration entry point (from account.data)
|
|
33
|
+
attr_accessor :entry_point
|
|
34
|
+
|
|
35
|
+
# Whether sample data is active (from account.settings)
|
|
36
|
+
attr_accessor :show_sample_data
|
|
37
|
+
|
|
32
38
|
# Attribute mapping from ruby-style variable name to JSON key.
|
|
33
39
|
def self.attribute_map
|
|
34
40
|
{
|
|
@@ -38,7 +44,9 @@ module SmplkitGeneratedClient::App
|
|
|
38
44
|
:'expires_at' => :'expires_at',
|
|
39
45
|
:'created_at' => :'created_at',
|
|
40
46
|
:'deleted_at' => :'deleted_at',
|
|
41
|
-
:'product_subscriptions' => :'product_subscriptions'
|
|
47
|
+
:'product_subscriptions' => :'product_subscriptions',
|
|
48
|
+
:'entry_point' => :'entry_point',
|
|
49
|
+
:'show_sample_data' => :'show_sample_data'
|
|
42
50
|
}
|
|
43
51
|
end
|
|
44
52
|
|
|
@@ -61,7 +69,9 @@ module SmplkitGeneratedClient::App
|
|
|
61
69
|
:'expires_at' => :'Time',
|
|
62
70
|
:'created_at' => :'Time',
|
|
63
71
|
:'deleted_at' => :'Time',
|
|
64
|
-
:'product_subscriptions' => :'Hash<String, Object>'
|
|
72
|
+
:'product_subscriptions' => :'Hash<String, Object>',
|
|
73
|
+
:'entry_point' => :'String',
|
|
74
|
+
:'show_sample_data' => :'Boolean'
|
|
65
75
|
}
|
|
66
76
|
end
|
|
67
77
|
|
|
@@ -71,7 +81,9 @@ module SmplkitGeneratedClient::App
|
|
|
71
81
|
:'expires_at',
|
|
72
82
|
:'created_at',
|
|
73
83
|
:'deleted_at',
|
|
74
|
-
:'product_subscriptions'
|
|
84
|
+
:'product_subscriptions',
|
|
85
|
+
:'entry_point',
|
|
86
|
+
:'show_sample_data'
|
|
75
87
|
])
|
|
76
88
|
end
|
|
77
89
|
|
|
@@ -126,6 +138,14 @@ module SmplkitGeneratedClient::App
|
|
|
126
138
|
self.product_subscriptions = value
|
|
127
139
|
end
|
|
128
140
|
end
|
|
141
|
+
|
|
142
|
+
if attributes.key?(:'entry_point')
|
|
143
|
+
self.entry_point = attributes[:'entry_point']
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if attributes.key?(:'show_sample_data')
|
|
147
|
+
self.show_sample_data = attributes[:'show_sample_data']
|
|
148
|
+
end
|
|
129
149
|
end
|
|
130
150
|
|
|
131
151
|
# Show invalid properties with the reasons. Usually used together with valid?
|
|
@@ -202,7 +222,9 @@ module SmplkitGeneratedClient::App
|
|
|
202
222
|
expires_at == o.expires_at &&
|
|
203
223
|
created_at == o.created_at &&
|
|
204
224
|
deleted_at == o.deleted_at &&
|
|
205
|
-
product_subscriptions == o.product_subscriptions
|
|
225
|
+
product_subscriptions == o.product_subscriptions &&
|
|
226
|
+
entry_point == o.entry_point &&
|
|
227
|
+
show_sample_data == o.show_sample_data
|
|
206
228
|
end
|
|
207
229
|
|
|
208
230
|
# @see the `==` method
|
|
@@ -214,7 +236,7 @@ module SmplkitGeneratedClient::App
|
|
|
214
236
|
# Calculates hash code according to all attributes.
|
|
215
237
|
# @return [Integer] Hash code
|
|
216
238
|
def hash
|
|
217
|
-
[name, key, has_stripe_customer, expires_at, created_at, deleted_at, product_subscriptions].hash
|
|
239
|
+
[name, key, has_stripe_customer, expires_at, created_at, deleted_at, product_subscriptions, entry_point, show_sample_data].hash
|
|
218
240
|
end
|
|
219
241
|
|
|
220
242
|
# Builds the object from hash
|
|
@@ -19,11 +19,37 @@ module SmplkitGeneratedClient::App
|
|
|
19
19
|
|
|
20
20
|
attr_accessor :password
|
|
21
21
|
|
|
22
|
+
# Registration entry point. Allowed: login, get_started, live_demo, unknown. Defaults to unknown when omitted.
|
|
23
|
+
attr_accessor :entry_point
|
|
24
|
+
|
|
25
|
+
class EnumAttributeValidator
|
|
26
|
+
attr_reader :datatype
|
|
27
|
+
attr_reader :allowable_values
|
|
28
|
+
|
|
29
|
+
def initialize(datatype, allowable_values)
|
|
30
|
+
@allowable_values = allowable_values.map do |value|
|
|
31
|
+
case datatype.to_s
|
|
32
|
+
when /Integer/i
|
|
33
|
+
value.to_i
|
|
34
|
+
when /Float/i
|
|
35
|
+
value.to_f
|
|
36
|
+
else
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def valid?(value)
|
|
43
|
+
!value || allowable_values.include?(value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
22
47
|
# Attribute mapping from ruby-style variable name to JSON key.
|
|
23
48
|
def self.attribute_map
|
|
24
49
|
{
|
|
25
50
|
:'email' => :'email',
|
|
26
|
-
:'password' => :'password'
|
|
51
|
+
:'password' => :'password',
|
|
52
|
+
:'entry_point' => :'entry_point'
|
|
27
53
|
}
|
|
28
54
|
end
|
|
29
55
|
|
|
@@ -41,13 +67,15 @@ module SmplkitGeneratedClient::App
|
|
|
41
67
|
def self.openapi_types
|
|
42
68
|
{
|
|
43
69
|
:'email' => :'String',
|
|
44
|
-
:'password' => :'String'
|
|
70
|
+
:'password' => :'String',
|
|
71
|
+
:'entry_point' => :'String'
|
|
45
72
|
}
|
|
46
73
|
end
|
|
47
74
|
|
|
48
75
|
# List of attributes with nullable: true
|
|
49
76
|
def self.openapi_nullable
|
|
50
77
|
Set.new([
|
|
78
|
+
:'entry_point'
|
|
51
79
|
])
|
|
52
80
|
end
|
|
53
81
|
|
|
@@ -78,6 +106,10 @@ module SmplkitGeneratedClient::App
|
|
|
78
106
|
else
|
|
79
107
|
self.password = nil
|
|
80
108
|
end
|
|
109
|
+
|
|
110
|
+
if attributes.key?(:'entry_point')
|
|
111
|
+
self.entry_point = attributes[:'entry_point']
|
|
112
|
+
end
|
|
81
113
|
end
|
|
82
114
|
|
|
83
115
|
# Show invalid properties with the reasons. Usually used together with valid?
|
|
@@ -112,6 +144,8 @@ module SmplkitGeneratedClient::App
|
|
|
112
144
|
return false if @password.nil?
|
|
113
145
|
return false if @password.to_s.length > 128
|
|
114
146
|
return false if @password.to_s.length < 8
|
|
147
|
+
entry_point_validator = EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
|
|
148
|
+
return false unless entry_point_validator.valid?(@entry_point)
|
|
115
149
|
true
|
|
116
150
|
end
|
|
117
151
|
|
|
@@ -143,13 +177,24 @@ module SmplkitGeneratedClient::App
|
|
|
143
177
|
@password = password
|
|
144
178
|
end
|
|
145
179
|
|
|
180
|
+
# Custom attribute writer method checking allowed values (enum).
|
|
181
|
+
# @param [Object] entry_point Object to be assigned
|
|
182
|
+
def entry_point=(entry_point)
|
|
183
|
+
validator = EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
|
|
184
|
+
unless validator.valid?(entry_point)
|
|
185
|
+
fail ArgumentError, "invalid value for \"entry_point\", must be one of #{validator.allowable_values}."
|
|
186
|
+
end
|
|
187
|
+
@entry_point = entry_point
|
|
188
|
+
end
|
|
189
|
+
|
|
146
190
|
# Checks equality by comparing each attribute.
|
|
147
191
|
# @param [Object] Object to be compared
|
|
148
192
|
def ==(o)
|
|
149
193
|
return true if self.equal?(o)
|
|
150
194
|
self.class == o.class &&
|
|
151
195
|
email == o.email &&
|
|
152
|
-
password == o.password
|
|
196
|
+
password == o.password &&
|
|
197
|
+
entry_point == o.entry_point
|
|
153
198
|
end
|
|
154
199
|
|
|
155
200
|
# @see the `==` method
|
|
@@ -161,7 +206,7 @@ module SmplkitGeneratedClient::App
|
|
|
161
206
|
# Calculates hash code according to all attributes.
|
|
162
207
|
# @return [Integer] Hash code
|
|
163
208
|
def hash
|
|
164
|
-
[email, password].hash
|
|
209
|
+
[email, password, entry_point].hash
|
|
165
210
|
end
|
|
166
211
|
|
|
167
212
|
# Builds the object from hash
|
|
@@ -69,4 +69,16 @@ describe SmplkitGeneratedClient::App::Account do
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
describe 'test attribute "entry_point"' do
|
|
73
|
+
it 'should work' do
|
|
74
|
+
# assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'test attribute "show_sample_data"' do
|
|
79
|
+
it 'should work' do
|
|
80
|
+
# assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
72
84
|
end
|
|
@@ -39,4 +39,14 @@ describe SmplkitGeneratedClient::App::RegisterRequest do
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
describe 'test attribute "entry_point"' do
|
|
43
|
+
it 'should work' do
|
|
44
|
+
# assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
|
|
45
|
+
# validator = Petstore::EnumTest::EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
|
|
46
|
+
# validator.allowable_values.each do |value|
|
|
47
|
+
# expect { instance.entry_point = value }.not_to raise_error
|
|
48
|
+
# end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
end
|
data/lib/smplkit/ws.rb
CHANGED
|
@@ -1,42 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
require "async"
|
|
5
|
+
require "async/http/endpoint"
|
|
6
|
+
require "async/websocket/client"
|
|
3
7
|
require "concurrent"
|
|
4
8
|
|
|
5
9
|
module Smplkit
|
|
6
10
|
# Manages a single WebSocket connection to the app service event gateway.
|
|
7
11
|
#
|
|
8
12
|
# A single +SharedWebSocket+ instance is shared across all product modules
|
|
9
|
-
# (config, flags) within one +Smplkit::Client+. Product modules
|
|
10
|
-
# listeners for specific event types; the shared connection
|
|
11
|
-
# incoming events to the appropriate listeners.
|
|
13
|
+
# (config, flags, logging) within one +Smplkit::Client+. Product modules
|
|
14
|
+
# register listeners for specific event types; the shared connection
|
|
15
|
+
# dispatches incoming events to the appropriate listeners.
|
|
12
16
|
#
|
|
13
|
-
# The connection runs on a dedicated SDK-owned thread
|
|
14
|
-
#
|
|
17
|
+
# The connection runs on a dedicated SDK-owned thread that hosts the
|
|
18
|
+
# +Async+ reactor and the underlying +async-websocket+ I/O. Public
|
|
19
|
+
# methods are thread-safe and non-blocking.
|
|
20
|
+
#
|
|
21
|
+
# Gateway protocol (mirrors the Python reference in +smplkit._ws+):
|
|
15
22
|
#
|
|
16
|
-
# The app service gateway protocol:
|
|
17
23
|
# - Connect to +wss://app.<base_domain>/api/ws/v1/events?api_key={key}+
|
|
18
24
|
# - Receive +{"type": "connected"}+ on success
|
|
19
25
|
# - Receive events: +{"event": "config_changed", ...}+, etc.
|
|
20
|
-
# - No subscribe message
|
|
26
|
+
# - No subscribe message — the API key determines the account
|
|
21
27
|
# - Heartbeat: server sends +"ping"+ (text), client responds with +"pong"+
|
|
22
28
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
29
|
+
# On disconnect the reactor reconnects with exponential backoff
|
|
30
|
+
# (1, 2, 4, 8, 16, 32, 60 seconds, then capped). +stop+ closes the
|
|
31
|
+
# connection from the outer thread; the reader exits and the daemon
|
|
32
|
+
# thread terminates.
|
|
27
33
|
class SharedWebSocket
|
|
28
34
|
BACKOFF_SCHEDULE = [1, 2, 4, 8, 16, 32, 60].freeze
|
|
29
35
|
|
|
36
|
+
USER_AGENT = "smplkit-ruby-sdk/#{Smplkit::VERSION}".freeze
|
|
37
|
+
|
|
30
38
|
def initialize(app_base_url:, api_key:, metrics: nil)
|
|
31
39
|
@app_base_url = app_base_url
|
|
32
40
|
@api_key = api_key
|
|
33
41
|
@metrics = metrics
|
|
34
|
-
@listeners =
|
|
42
|
+
@listeners = Hash.new { |h, k| h[k] = [] }
|
|
35
43
|
@listeners_lock = Mutex.new
|
|
36
44
|
@connection_status = "disconnected"
|
|
37
45
|
@closed = false
|
|
46
|
+
@ws_thread = nil
|
|
47
|
+
@connection = nil
|
|
48
|
+
@connection_lock = Mutex.new
|
|
38
49
|
end
|
|
39
50
|
|
|
51
|
+
# ----- Listener registration ------------------------------------
|
|
52
|
+
|
|
40
53
|
def on(event_name, &callback)
|
|
41
54
|
@listeners_lock.synchronize { @listeners[event_name] << callback }
|
|
42
55
|
end
|
|
@@ -45,6 +58,9 @@ module Smplkit
|
|
|
45
58
|
@listeners_lock.synchronize { @listeners[event_name].delete(callback) }
|
|
46
59
|
end
|
|
47
60
|
|
|
61
|
+
# Dispatch +data+ to every listener registered for +event_name+.
|
|
62
|
+
# Listener exceptions are caught and logged; one bad listener never
|
|
63
|
+
# blocks the rest.
|
|
48
64
|
def dispatch(event_name, data)
|
|
49
65
|
callbacks = @listeners_lock.synchronize { @listeners[event_name].dup }
|
|
50
66
|
callbacks.each do |cb|
|
|
@@ -54,39 +70,186 @@ module Smplkit
|
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
|
|
73
|
+
# ----- Connection status ----------------------------------------
|
|
74
|
+
|
|
57
75
|
attr_reader :connection_status
|
|
58
76
|
|
|
59
|
-
#
|
|
60
|
-
# Production wiring overrides this from the I/O thread once the gateway
|
|
61
|
-
# confirms the handshake.
|
|
62
|
-
def mark_connected!
|
|
63
|
-
@connection_status = "connected"
|
|
64
|
-
end
|
|
77
|
+
# ----- Lifecycle ------------------------------------------------
|
|
65
78
|
|
|
66
79
|
def start
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
return if @ws_thread&.alive?
|
|
81
|
+
|
|
82
|
+
Smplkit.debug("websocket", "starting shared WebSocket background thread")
|
|
83
|
+
@closed = false
|
|
84
|
+
@connection_status = "connecting"
|
|
85
|
+
@ws_thread = Thread.new { run_reactor }
|
|
86
|
+
@ws_thread.name = "smplkit-shared-ws" if @ws_thread.respond_to?(:name=)
|
|
72
87
|
end
|
|
73
88
|
|
|
74
89
|
def stop
|
|
90
|
+
Smplkit.debug("websocket", "stopping shared WebSocket")
|
|
75
91
|
@closed = true
|
|
76
92
|
@connection_status = "disconnected"
|
|
93
|
+
close_active_connection
|
|
94
|
+
thread = @ws_thread
|
|
95
|
+
@ws_thread = nil
|
|
96
|
+
return unless thread
|
|
97
|
+
|
|
98
|
+
thread.join(2.0)
|
|
99
|
+
thread.kill if thread.alive?
|
|
77
100
|
end
|
|
78
101
|
|
|
102
|
+
# ----- URL builder ----------------------------------------------
|
|
103
|
+
|
|
79
104
|
def build_ws_url
|
|
80
105
|
url = @app_base_url.dup
|
|
81
|
-
ws_url =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
ws_url =
|
|
107
|
+
if url.start_with?("https://")
|
|
108
|
+
"wss://#{url[("https://".length)..]}"
|
|
109
|
+
elsif url.start_with?("http://")
|
|
110
|
+
"ws://#{url[("http://".length)..]}"
|
|
111
|
+
else
|
|
112
|
+
"wss://#{url}"
|
|
113
|
+
end
|
|
88
114
|
ws_url = ws_url.chomp("/")
|
|
89
115
|
"#{ws_url}/api/ws/v1/events?api_key=#{@api_key}"
|
|
90
116
|
end
|
|
117
|
+
|
|
118
|
+
# ----- Inbound message handling (extracted for tests) -----------
|
|
119
|
+
|
|
120
|
+
# Process a single inbound text frame the way the live reactor does:
|
|
121
|
+
# +"ping"+ → call +send_pong+ with +"pong"+; otherwise parse JSON and,
|
|
122
|
+
# if a +"event"+ key is present, dispatch to listeners.
|
|
123
|
+
#
|
|
124
|
+
# Returns one of +:ping+, +:event+, +:no_event+, +:unparseable+ for the
|
|
125
|
+
# caller to log/observe; the live reactor ignores the return value.
|
|
126
|
+
def handle_inbound(text, send_pong:)
|
|
127
|
+
if text == "ping"
|
|
128
|
+
send_pong.call("pong")
|
|
129
|
+
return :ping
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
data =
|
|
133
|
+
begin
|
|
134
|
+
JSON.parse(text)
|
|
135
|
+
rescue JSON::ParserError
|
|
136
|
+
return :unparseable
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
event = data["event"]
|
|
140
|
+
if event
|
|
141
|
+
dispatch(event, data)
|
|
142
|
+
:event
|
|
143
|
+
else
|
|
144
|
+
:no_event
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def run_reactor
|
|
151
|
+
Sync do |task|
|
|
152
|
+
ws_main(task)
|
|
153
|
+
end
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
Smplkit.debug("websocket", "shared WebSocket thread exited unexpectedly: #{e.class}: #{e.message}")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ws_main(task)
|
|
159
|
+
connect(task)
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
return if @closed
|
|
162
|
+
|
|
163
|
+
Smplkit.debug(
|
|
164
|
+
"websocket",
|
|
165
|
+
"connection failed on startup (url: #{safe_url}): #{e.class}: #{e.message}"
|
|
166
|
+
)
|
|
167
|
+
reconnect(task)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def connect(task)
|
|
171
|
+
url = build_ws_url
|
|
172
|
+
@connection_status = "connecting"
|
|
173
|
+
Smplkit.debug("websocket", "connecting to #{safe_url}")
|
|
174
|
+
|
|
175
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
176
|
+
headers = { "user-agent" => USER_AGENT }
|
|
177
|
+
connection = Async::WebSocket::Client.connect(endpoint, headers: headers)
|
|
178
|
+
@connection_lock.synchronize { @connection = connection }
|
|
179
|
+
Smplkit.debug("websocket", "WebSocket connected, waiting for confirmation")
|
|
180
|
+
|
|
181
|
+
raw = connection.read
|
|
182
|
+
data = JSON.parse(message_to_string(raw))
|
|
183
|
+
if data["type"] == "error"
|
|
184
|
+
err = data["message"]
|
|
185
|
+
Smplkit.debug("websocket", "connection error from server: #{err.inspect}")
|
|
186
|
+
raise "Connection error: #{err}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
@connection_status = "connected"
|
|
190
|
+
@metrics&.record_gauge("platform.websocket_connections", 1, unit: "connections")
|
|
191
|
+
receive_loop(task, connection)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def receive_loop(task, connection)
|
|
195
|
+
until @closed
|
|
196
|
+
message = connection.read
|
|
197
|
+
break if message.nil?
|
|
198
|
+
|
|
199
|
+
text = message_to_string(message)
|
|
200
|
+
handle_inbound(text, send_pong: ->(reply) { connection.write(reply) })
|
|
201
|
+
end
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
return if @closed
|
|
204
|
+
|
|
205
|
+
Smplkit.debug("websocket", "receive loop error: #{e.class}: #{e.message}")
|
|
206
|
+
@connection_status = "reconnecting"
|
|
207
|
+
@metrics&.record_gauge("platform.websocket_connections", 0, unit: "connections")
|
|
208
|
+
reconnect(task)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def reconnect(task)
|
|
212
|
+
attempt = 0
|
|
213
|
+
until @closed
|
|
214
|
+
delay = BACKOFF_SCHEDULE[[attempt, BACKOFF_SCHEDULE.length - 1].min]
|
|
215
|
+
Smplkit.debug("websocket", "reconnecting in #{delay}s (attempt #{attempt + 1})")
|
|
216
|
+
task.sleep(delay)
|
|
217
|
+
return if @closed
|
|
218
|
+
|
|
219
|
+
begin
|
|
220
|
+
connect(task)
|
|
221
|
+
return
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
Smplkit.debug("websocket", "reconnect attempt #{attempt + 1} failed: #{e.class}: #{e.message}")
|
|
224
|
+
attempt += 1
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def message_to_string(message)
|
|
230
|
+
return message if message.is_a?(String)
|
|
231
|
+
return message.to_str if message.respond_to?(:to_str)
|
|
232
|
+
|
|
233
|
+
message.to_s
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def close_active_connection
|
|
237
|
+
conn = @connection_lock.synchronize do
|
|
238
|
+
c = @connection
|
|
239
|
+
@connection = nil
|
|
240
|
+
c
|
|
241
|
+
end
|
|
242
|
+
return unless conn
|
|
243
|
+
|
|
244
|
+
begin
|
|
245
|
+
conn.close
|
|
246
|
+
rescue StandardError => e
|
|
247
|
+
Smplkit.debug("websocket", "close raised: #{e.class}: #{e.message}")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def safe_url
|
|
252
|
+
build_ws_url.split("?", 2).first
|
|
253
|
+
end
|
|
91
254
|
end
|
|
92
255
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smplkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Smpl Solutions LLC
|
|
@@ -9,6 +9,48 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.39'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.39'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: async-http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.95'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.95'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: async-websocket
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.30'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.30'
|
|
12
54
|
- !ruby/object:Gem::Dependency
|
|
13
55
|
name: concurrent-ruby
|
|
14
56
|
requirement: !ruby/object:Gem::Requirement
|