schwab_rb 1.0.4 → 1.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80e57e5f4da3062980fd00cfbce3661185d36a6ccd34a87d51c3d6cd8c3400f8
4
- data.tar.gz: 1f2ec23196743009009fda3942719b680e474a25dbf54cb5d412f25203140abf
3
+ metadata.gz: 931dc11bd7c59a66e180069c36ab7f6dd44cc068db8aa3c5b58ece0aaf830a6e
4
+ data.tar.gz: e9abc3aa0cce142ffe3ac9af5cca9ee9fa1e5d1fac7209e9f39c929c1f6f8f1c
5
5
  SHA512:
6
- metadata.gz: cd23d7763c0ee77ec39a45bb653d4c65f4a19ae1962b12c9c3c51cd1b7035114158fb77f36e090a3909d2d08dd88f32b6626d92fa5f038b3a563ff369dbfce49
7
- data.tar.gz: 17bab9ddddca12ed8e69b580936c4f166088cd8e919d530ad690375d0bf674fc4eab539cfd5b7c241f56358c2182cca59e9d3f4b4b3e6db15dcfa8efbe67ae52
6
+ metadata.gz: 53af08e726ae79f7b672df90cab4486b88b1ffaedc19ebc7b666bde8362f4fe49667f78f8ffd390be215b0c975895f21b7b4a7ae34addbb41d94bbe4c3030807
7
+ data.tar.gz: 1ce70df60193ac32c02b50eb978c96a640ff75bf2715504824341daf4e51ec8a6c2e466a2598509a66195cfbba21a7724bba6f6b6a901946c5b5352df595f934
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.6] - 2026-11-13
4
+
5
+ ### Changed
6
+ - Stream services now retain one callback; registering another block for the same service replaces the previous callback.
7
+ - `Stream::AsyncClient` supports dynamic `add` and `unsub` operations.
8
+
9
+ ### Fixed
10
+ - Prevent duplicate callback invocation on repeated subscription additions and preserve callbacks when symbols are unsubscribed.
11
+ - Allow callback registration before symbols are supplied without sending an empty subscription.
12
+
13
+ ## [1.0.5] - 2026-10-19
14
+
15
+ ### Added
16
+ - Sanitized stream subscription logging (`SUBS`, `ADD`, `UNSUBS`) at INFO level with service, command, request ID, and keys.
17
+ - Stream subscription acknowledgement response logging at INFO level with service, command, response code, message, and request ID.
18
+
19
+ ### Security
20
+ - Redacted `Authorization` access tokens in WebSocket outbound debug logging.
21
+
3
22
  ## [1.0.4] - 2026-09-21
4
23
 
5
24
  ### Added
data/README.md CHANGED
@@ -205,6 +205,12 @@ stream.unsub(:level_one_equities, symbols: ["SPY", "QQQ"])
205
205
  stream.stop
206
206
  ```
207
207
 
208
+ Each service has one callback. Passing a block to `on` or `add` replaces its
209
+ callback; calls without a block preserve it, including after all symbols are
210
+ unsubscribed. You can register a callback with `on` before supplying symbols;
211
+ fields specified there are used when symbols are later added without fields.
212
+ Both `Stream::Client` and `Stream::AsyncClient` support `on`, `add`, and `unsub`.
213
+
208
214
  ## Data Objects
209
215
 
210
216
  The gem includes structured data objects for better handling of API responses. Most API methods support a `return_data_objects` parameter (defaults to `true`) which returns parsed Ruby objects instead of raw JSON responses:
@@ -13,6 +13,12 @@ module SchwabRb
13
13
  @stream.on(service_symbol, symbols: symbols, fields: fields, &block)
14
14
  self
15
15
  end
16
+ alias add on
17
+
18
+ def unsub(service_symbol, symbols: nil)
19
+ @stream.unsub(service_symbol, symbols: symbols)
20
+ self
21
+ end
16
22
 
17
23
  def start
18
24
  @stream.start
@@ -19,6 +19,7 @@ module SchwabRb
19
19
  def initialize(client)
20
20
  @client = client
21
21
  @handlers = {}
22
+ @service_fields = {}
22
23
  @subscriptions = {}
23
24
  @connection = nil
24
25
  @message_builder = nil
@@ -34,16 +35,18 @@ module SchwabRb
34
35
  service_name = Services.lookup(service_symbol)
35
36
  resolved_fields = fields ? Fields.resolve(service_name, fields) : nil
36
37
 
37
- handler = { symbols: Array(symbols).map(&:to_s), fields: resolved_fields, callback: block }
38
- @handlers[service_name] ||= []
39
- @handlers[service_name] << handler
38
+ @handlers[service_name] = block if block
39
+ @service_fields[service_name] = resolved_fields unless fields.nil?
40
+ symbol_list = Array(symbols).map(&:to_s)
41
+ return self if symbol_list.empty?
40
42
 
43
+ subscription = { symbols: symbol_list, fields: resolved_fields || @service_fields[service_name] }
41
44
  @subscriptions[service_name] = merge_subscription(
42
45
  @subscriptions[service_name],
43
- handler
46
+ subscription
44
47
  )
45
48
 
46
- send_subscription(service_name, handler) if @connected
49
+ send_subscription(service_name, subscription) if @connected
47
50
 
48
51
  self
49
52
  end
@@ -59,19 +62,12 @@ module SchwabRb
59
62
  @subscriptions.delete(service_name) if @subscriptions[service_name][:symbols].empty?
60
63
  end
61
64
 
62
- if @handlers[service_name]
63
- @handlers[service_name].each do |h|
64
- h[:symbols] -= symbols_to_remove
65
- end
66
- @handlers[service_name].reject! { |h| h[:symbols].empty? }
67
- @handlers.delete(service_name) if @handlers[service_name].empty?
68
- end
69
-
70
65
  if @connected
71
66
  request = @message_builder.build_request(
72
67
  service_name, Commands::UNSUBS,
73
68
  keys: symbols_to_remove
74
69
  )
70
+ log_subscription_request(request)
75
71
  send_message(@message_builder.wrap_requests(request))
76
72
  end
77
73
 
@@ -176,6 +172,7 @@ module SchwabRb
176
172
  keys: sub[:symbols],
177
173
  fields: sub[:fields]
178
174
  )
175
+ log_subscription_request(request)
179
176
  send_message(@message_builder.wrap_requests(request))
180
177
  end
181
178
  end
@@ -186,6 +183,7 @@ module SchwabRb
186
183
  keys: handler[:symbols],
187
184
  fields: handler[:fields]
188
185
  )
186
+ log_subscription_request(request)
189
187
  send_message(@message_builder.wrap_requests(request))
190
188
  end
191
189
 
@@ -203,20 +201,43 @@ module SchwabRb
203
201
  if parsed.key?("data")
204
202
  parsed["data"].each do |data_entry|
205
203
  service = data_entry["service"]
206
- handlers = @handlers[service]
207
- next unless handlers
208
-
209
- handlers.each { |h| h[:callback]&.call(data_entry) }
204
+ @handlers[service]&.call(data_entry)
210
205
  end
211
206
  end
212
207
 
213
208
  return unless parsed.key?("response")
214
209
 
215
210
  parsed["response"].each do |resp|
216
- SchwabRb::Logger.logger.debug("Stream response: #{resp}")
211
+ log_stream_response(resp)
217
212
  end
218
213
  end
219
214
 
215
+ def log_subscription_request(request)
216
+ service = request["service"]
217
+ command = request["command"]
218
+ request_id = request["requestid"]
219
+ keys = request.dig("parameters", "keys")
220
+ SchwabRb::Logger.logger.info(
221
+ "Stream subscription request: service=#{service} command=#{command} " \
222
+ "request_id=#{request_id} keys=#{keys}"
223
+ )
224
+ end
225
+
226
+ def log_stream_response(resp)
227
+ service = resp["service"]
228
+ command = resp["command"]
229
+ request_id = resp["requestid"]
230
+ content = resp["content"] || {}
231
+ code = content["code"]
232
+ msg = content["msg"]
233
+
234
+ SchwabRb::Logger.logger.info(
235
+ "Stream response: service=#{service} command=#{command} " \
236
+ "code=#{code} msg=#{msg.inspect} request_id=#{request_id}"
237
+ )
238
+ SchwabRb::Logger.logger.debug("Raw stream response: #{resp}")
239
+ end
240
+
220
241
  def handle_disconnect(error)
221
242
  @connected = false
222
243
  begin
@@ -255,7 +276,10 @@ module SchwabRb
255
276
  @write_mutex.synchronize do
256
277
  return unless @connection
257
278
 
258
- SchwabRb::Logger.logger.debug("Stream sending: #{json_string}")
279
+ if SchwabRb::Logger.logger.debug?
280
+ sanitized = json_string.gsub(/"Authorization"\s*:\s*"[^"]*"/, '"Authorization":"[REDACTED]"')
281
+ SchwabRb::Logger.logger.debug("Stream sending: #{sanitized}")
282
+ end
259
283
  @connection.write(json_string)
260
284
  @connection.flush
261
285
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabRb
4
- VERSION = "1.0.4"
4
+ VERSION = "1.0.6"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwab_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Platta
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-21 00:00:00.000000000 Z
10
+ date: 2026-11-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async