lightstreamer 0.6 → 0.7

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
  SHA1:
3
- metadata.gz: 8c5dce2a119bf535faac9e10b36a13624cf57038
4
- data.tar.gz: 64bd43f2f8e122608e552e23dfa38047a0e3b53f
3
+ metadata.gz: 67492665643def7bc43ea5ab072416c91d7e494d
4
+ data.tar.gz: 3f47646a1ff2d3c939c65588304e57234f893490
5
5
  SHA512:
6
- metadata.gz: 3261d66e3032ff0f0d324b02e7efda9626fd9a207921247307cbc6d0cbb97698131d423185012ae1b1741af1c50e867a886b4ed5fe30d1093bc6e9c7ec85a7b5
7
- data.tar.gz: a8ad81e667183f8d867162f0d0f2733ec7c9d15541508f22b94c34638f944d9343b9cea862bf0f7407475a141f487d281c45bff04644eed37c1c02990b8bd75a
6
+ metadata.gz: 7510f00c805e9f83758e91b82c8682f22472d9c367a5a348faa4e46c8422e479c530bd96210c5eced1d7f56537ad39df14b6cf30dbf9d1a4787e1ea16695e6e5
7
+ data.tar.gz: 184073f14a2e2d34b9f208201526d6357a991d2a4bb113f40da21f80b112a0487f1177c12a24bd715a0e0c5b81d59997d9b6d64b603cff9c7ef89069e0455e02
data/CHANGELOG.md CHANGED
@@ -1,6 +1,26 @@
1
1
  # Lightstreamer Changelog
2
2
 
3
- ### 0.6 — July 26, 2016
3
+ ### 0.7 — July 31, 2016
4
+
5
+ - Refactored subscription handling to be more object-oriented, subscriptions are now created using
6
+ `Lightstreamer::Session#build_subscription` and there are new `#start`, `#stop` and `#unsilence` methods on
7
+ `Lightstreamer::Subscription` that control a subscription's current state
8
+ - Added `Lightstreamer::Session#bulk_subscription_start` for efficiently starting a number of subscriptions at once
9
+ - Added support for sending custom messages using `Lightstreamer::Session#send_message`, both synchronous and
10
+ asynchronous messages are supported, and the results of asynchronous messages can be checked using the new
11
+ `Lightstreamer::Session#on_message_result` callback
12
+ - Added support for setting initial subscription data
13
+ - Added support for changing a subscription's requested update frequency after it has been started
14
+ - Added support for setting and altering the session's requested maximum bandwidth
15
+ - Added support for requesting snapshots on subscriptions
16
+ - Added `Lightstreamer::Session#session_id` to query the session ID
17
+ - Added `Lightstreamer::Subscription#active` to query whether the subscription is active and streaming data
18
+ - Stream connection bind requests are now sent to the custom control address if one was specified in the stream
19
+ connection header
20
+ - The command-line client now prints the session ID following successful connection
21
+ - Added —requested-maximum-bandwidth option
22
+
23
+ ### 0.6 — July 27, 2016
4
24
 
5
25
  - Switched to the `excon` HTTP library
6
26
  - Moved all subclasses of `Lightstreamer::LightstreamerError` into the `Lightstreamer::Errors` module
data/README.md CHANGED
@@ -44,9 +44,9 @@ session = Lightstreamer::Session.new server_url: 'http://push.lightstreamer.com'
44
44
  session.connect
45
45
 
46
46
  # Create a new subscription that subscribes to thirty items and to four fields on each item
47
- subscription = Lightstreamer::Subscription.new items: (1..30).map { |i| "item#{i}" },
48
- fields: [:ask, :bid, :stock_name, :time],
49
- mode: :merge, adapter: 'QUOTE_ADAPTER'
47
+ subscription = session.build_subscription items: (1..30).map { |i| "item#{i}" },
48
+ fields: [:ask, :bid, :stock_name, :time],
49
+ mode: :merge, adapter: 'QUOTE_ADAPTER'
50
50
 
51
51
  # Create a thread-safe queue
52
52
  queue = Queue.new
@@ -57,8 +57,8 @@ subscription.on_data do |subscription, item_name, item_data, new_values|
57
57
  queue.push item_data
58
58
  end
59
59
 
60
- # Activate the subscription
61
- session.subscribe subscription
60
+ # Start streaming data for the subscription
61
+ subscription.start
62
62
 
63
63
  # Loop printing out new data as soon as it becomes available on the queue
64
64
  loop do
data/lib/lightstreamer.rb CHANGED
@@ -4,12 +4,14 @@ require 'uri'
4
4
 
5
5
  require 'lightstreamer/cli/main'
6
6
  require 'lightstreamer/cli/commands/stream_command'
7
- require 'lightstreamer/control_connection'
8
7
  require 'lightstreamer/errors'
9
- require 'lightstreamer/line_buffer'
8
+ require 'lightstreamer/messages/end_of_snapshot_message'
10
9
  require 'lightstreamer/messages/overflow_message'
10
+ require 'lightstreamer/messages/send_message_outcome_message'
11
11
  require 'lightstreamer/messages/update_message'
12
+ require 'lightstreamer/post_request'
12
13
  require 'lightstreamer/session'
14
+ require 'lightstreamer/stream_buffer'
13
15
  require 'lightstreamer/stream_connection'
14
16
  require 'lightstreamer/stream_connection_header'
15
17
  require 'lightstreamer/subscription'
@@ -8,51 +8,57 @@ module Lightstreamer
8
8
  option :username, desc: 'The username for the session'
9
9
  option :password, desc: 'The password for the session'
10
10
  option :adapter_set, desc: 'The name of the adapter set for the session'
11
+ option :requested_maximum_bandwidth, type: :numeric, desc: 'The requested maximum bandwidth, in kbps'
11
12
  option :adapter, desc: 'The name of the data adapter to stream data from'
12
13
  option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
13
14
  option :fields, type: :array, required: true, desc: 'The field(s) to stream'
14
15
  option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
15
16
  option :selector, desc: 'The selector for table items'
17
+ option :snapshot, type: :boolean, desc: 'Whether to send snapshot data for the items'
16
18
  option :maximum_update_frequency, desc: 'The maximum number of updates per second for each item'
17
19
 
18
20
  def stream
19
- session = create_session
20
- session.connect
21
-
22
21
  @queue = Queue.new
23
22
 
24
- session.subscribe create_subscription
23
+ create_session
24
+ create_subscription
25
25
 
26
26
  loop do
27
27
  puts @queue.pop unless @queue.empty?
28
28
 
29
- raise session.error if session.error
29
+ raise @session.error if @session.error
30
30
  end
31
31
  end
32
32
 
33
33
  private
34
34
 
35
- # Creates a new session from the specified options.
36
35
  def create_session
37
- Lightstreamer::Session.new server_url: options[:server_url], username: options[:username],
38
- password: options[:password], adapter_set: options[:adapter_set]
36
+ @session = Lightstreamer::Session.new session_options
37
+ @session.connect
38
+ @session.on_message_result(&method(:on_message_result))
39
+
40
+ puts "Session ID: #{@session.session_id}"
39
41
  end
40
42
 
41
- # Creates a new subscription from the specified options.
42
43
  def create_subscription
43
- subscription = Lightstreamer::Subscription.new subscription_options
44
+ subscription = @session.build_subscription subscription_options
44
45
 
45
46
  subscription.on_data(&method(:on_data))
46
47
  subscription.on_overflow(&method(:on_overflow))
48
+ subscription.on_end_of_snapshot(&method(:on_end_of_snapshot))
49
+
50
+ subscription.start
51
+ end
47
52
 
48
- subscription
53
+ def session_options
54
+ { server_url: options[:server_url], username: options[:username], password: options[:password],
55
+ adapter_set: options[:adapter_set], requested_maximum_bandwidth: options[:requested_maximum_bandwidth] }
49
56
  end
50
57
 
51
58
  def subscription_options
52
- {
53
- items: options[:items], fields: options[:fields], mode: options[:mode], adapter: options[:adapter],
54
- maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector]
55
- }
59
+ { items: options[:items], fields: options[:fields], mode: options[:mode], adapter: options[:adapter],
60
+ maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector],
61
+ snapshot: options[:snapshot] }
56
62
  end
57
63
 
58
64
  def on_data(_subscription, item_name, _item_data, new_values)
@@ -62,6 +68,14 @@ module Lightstreamer
62
68
  def on_overflow(_subscription, item_name, overflow_size)
63
69
  @queue.push "Overflow of size #{overflow_size} on item #{item_name}"
64
70
  end
71
+
72
+ def on_message_result(sequence, numbers, error)
73
+ @queue.push "Message result for #{sequence}#{numbers} = #{error ? error.class : 'Done'}"
74
+ end
75
+
76
+ def on_end_of_snapshot(_subscription, item_name)
77
+ @queue.push "End of snapshot for item #{item_name}"
78
+ end
65
79
  end
66
80
  end
67
81
  end
@@ -95,6 +95,18 @@ module Lightstreamer
95
95
  class InvalidProgressiveNumberError < LightstreamerError
96
96
  end
97
97
 
98
+ # This error occurs when a send message request was refused as illegal by the metadata adapter.
99
+ class IllegalMessageError < LightstreamerError
100
+ end
101
+
102
+ # This error occurs when the elaboration of a sent message failed unexpectedly.
103
+ class MessageElaborationFailedError < LightstreamerError
104
+ end
105
+
106
+ # This error occurs when sent message(s) were not processed due to a timeout.
107
+ class MessagesSkippedByTimeoutError < LightstreamerError
108
+ end
109
+
98
110
  # This error is raised when the client version requested is not supported by the server.
99
111
  class ClientVersionNotSupportedError < LightstreamerError
100
112
  end
@@ -209,6 +221,10 @@ module Lightstreamer
209
221
  30 => Errors::SubscriptionsNotAllowedByLicenseError,
210
222
  32 => Errors::InvalidProgressiveNumberError,
211
223
  33 => Errors::InvalidProgressiveNumberError,
224
+ 34 => Errors::IllegalMessageError,
225
+ 35 => Errors::MessageElaborationFailedError,
226
+ 38 => Errors::MessagesSkippedByTimeoutError,
227
+ 39 => Errors::MessagesSkippedByTimeoutError,
212
228
  60 => Errors::ClientVersionNotSupportedError
213
229
  }.freeze
214
230
 
@@ -0,0 +1,27 @@
1
+ module Lightstreamer
2
+ # Helper class used by {Subscription} in order to parse incoming end-of-snapshot messages.
3
+ #
4
+ # @private
5
+ class EndOfSnapshotMessage
6
+ # The index of the item this end-of-snapshot message applies to.
7
+ #
8
+ # @return [Fixnum]
9
+ attr_accessor :item_index
10
+
11
+ class << self
12
+ # Attempts to parses the specified line as an end-of-snapshot message for the given table and items and returns an
13
+ # instance of {EndOfSnapshotMessage} on success, or `nil` on failure.
14
+ def parse(line, table_id, items)
15
+ message = new
16
+
17
+ match = line.match Regexp.new("^#{table_id},(\\d+),EOS$")
18
+ return unless match
19
+
20
+ message.item_index = match.captures[0].to_i - 1
21
+ return unless message.item_index < items.size
22
+
23
+ message
24
+ end
25
+ end
26
+ end
27
+ end
@@ -17,11 +17,11 @@ module Lightstreamer
17
17
  # Attempts to parses the specified line as an overflow message for the given table and items and returns an
18
18
  # instance of {OverflowMessage} on success, or `nil` on failure.
19
19
  def parse(line, table_id, items)
20
- message = new
21
-
22
20
  match = line.match table_regexp(table_id)
23
21
  return unless match
24
22
 
23
+ message = new
24
+
25
25
  message.item_index = match.captures[0].to_i - 1
26
26
  return unless message.item_index < items.size
27
27
 
@@ -0,0 +1,53 @@
1
+ module Lightstreamer
2
+ # Helper class used by {Session} in order to parse incoming overflow send message outcome messages.
3
+ #
4
+ # @private
5
+ class SendMessageOutcomeMessage
6
+ # The name of the sequence this message outcome is for.
7
+ #
8
+ # @return [String]
9
+ attr_accessor :sequence
10
+
11
+ # The message number(s) this message outcome is for. There will always be exactly one entry in this array except in
12
+ # the case where {#error} is a {MessagesSkippedByTimeoutError} in which case there may be more than one entry if
13
+ # multiple messages were skipped.
14
+ #
15
+ # @return [Array<Fixnum>]
16
+ attr_accessor :numbers
17
+
18
+ # If an error occurred processing the message then it will be set here.
19
+ #
20
+ # @return [LightstreamerError, nil]
21
+ attr_accessor :error
22
+
23
+ class << self
24
+ # Attempts to parses the specified line as a message outcome message and returns an instance of
25
+ # {SendMessageOutcomeMessage} on success, or `nil` on failure.
26
+ def parse(line)
27
+ match = line.match Regexp.new '^MSG,([A-Za-z0-9_]+),(\d*),(?:DONE|ERR,(\d*),(.*))$'
28
+ return unless match
29
+
30
+ message = new
31
+
32
+ message.sequence = match.captures[0]
33
+ message.numbers = [match.captures[1].to_i]
34
+ handle_error_outcome message, match.captures if match.captures.compact.size == 4
35
+
36
+ message
37
+ end
38
+
39
+ private
40
+
41
+ def handle_error_outcome(message, captures)
42
+ message.error = LightstreamerError.build captures[3], captures[2]
43
+
44
+ return unless captures[2].to_i == 39
45
+
46
+ last_number = message.numbers[0]
47
+ first_number = last_number - captures[3].to_i + 1
48
+
49
+ message.numbers = Array(first_number..last_number)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -17,11 +17,11 @@ module Lightstreamer
17
17
  # Attempts to parses the specified line as an update message for the given table, items, and fields, and returns
18
18
  # an instance of {UpdateMessage} on success, or `nil` on failure.
19
19
  def parse(line, table_id, items, fields)
20
- message = new
21
-
22
20
  match = line.match table_regexp(table_id, fields)
23
21
  return unless match
24
22
 
23
+ message = new
24
+
25
25
  message.item_index = match.captures[0].to_i - 1
26
26
  return unless message.item_index < items.size
27
27
 
@@ -0,0 +1,81 @@
1
+ module Lightstreamer
2
+ # This module contains helper methods for sending single and bulk POST requests to a Lightstreamer server and
3
+ # handling the possible error responses.
4
+ #
5
+ # @private
6
+ module PostRequest
7
+ module_function
8
+
9
+ # Sends a POST request to the specified Lightstreamer URL with the given query params. If an error occurs then a
10
+ # {LightstreamerError} subclass will be raised.
11
+ #
12
+ # @param [String] url The URL to send the POST request to.
13
+ # @param [Hash] query The POST request's query params.
14
+ def execute(url, query)
15
+ errors = bulk_execute url, [request_body(query)]
16
+ raise errors.first if errors.first
17
+ end
18
+
19
+ # Sends a POST request to the specified Lightstreamer URL that concatenates multiple individual POST request bodies
20
+ # into one to avoid sending lots of individual requests. The return value is an array with one entry per body and
21
+ # indicates the error state returned by the server for that body's request, or `nil` if no error occurred.
22
+ #
23
+ # @param [String] url The URL to send the POST request to.
24
+ # @param [Array<String>] bodies The individual POST request bodies that are to be sent together in one request.
25
+ # These should be created with {#request_body}.
26
+ #
27
+ # @return [Array<LightstreamerError, nil>] The execution result of each of the passed bodies. If an entry is `nil`
28
+ # then no error occurred when executing that body.
29
+ def bulk_execute(url, bodies)
30
+ response = Excon.post url, body: bodies.join("\r\n"), expects: 200, connect_timeout: 15
31
+
32
+ response_lines = response.body.split("\n").map(&:strip)
33
+
34
+ errors = []
35
+ errors << parse_error(response_lines) until response_lines.empty?
36
+
37
+ raise LightstreamerError if errors.size != bodies.size
38
+
39
+ errors
40
+ rescue Excon::Error => error
41
+ raise Errors::ConnectionError, error.message
42
+ end
43
+
44
+ # Returns the request body to send for a POST request with the given options.
45
+ #
46
+ # @param [Hash] query The POST request's query params.
47
+ #
48
+ # @return [String] The request body for the given query params.
49
+ def request_body(query)
50
+ params = {}
51
+
52
+ query.each do |key, value|
53
+ next if value.nil?
54
+ value = value.map(&:to_s).join(' ') if value.is_a? Array
55
+ params[key] = value
56
+ end
57
+
58
+ URI.encode_www_form params
59
+ end
60
+
61
+ # Parses the next error from the given lines that were returned by a POST request. The consumed lines are removed
62
+ # from the passed array.
63
+ #
64
+ # @param [Array<String>] response_lines
65
+ #
66
+ # @return [LightstreamerError, nil]
67
+ def parse_error(response_lines)
68
+ first_line = response_lines.shift
69
+
70
+ return nil if first_line == 'OK'
71
+ return Errors::SyncError.new if first_line == 'SYNC ERROR'
72
+
73
+ if first_line == 'ERROR'
74
+ error_code = response_lines.shift
75
+ LightstreamerError.build response_lines.shift, error_code
76
+ else
77
+ LightstreamerError.new first_line
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,6 +1,6 @@
1
1
  module Lightstreamer
2
- # This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class is the
3
- # primary interface for working with Lightstreamer.
2
+ # This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class forms the
3
+ # primary API for working with Lightstreamer.
4
4
  class Session
5
5
  # The URL of the Lightstreamer server to connect to. Set by {#initialize}.
6
6
  #
@@ -24,11 +24,16 @@ module Lightstreamer
24
24
 
25
25
  # If an error occurs on the stream connection that causes the session to terminate then details of the error will be
26
26
  # stored in this attribute. If the session is terminated as a result of calling {#disconnect} then the error will be
27
- # {SessionEndError}.
27
+ # {Errors::SessionEndError}.
28
28
  #
29
29
  # @return [LightstreamerError, nil]
30
30
  attr_reader :error
31
31
 
32
+ # The server-side bandwidth constraint on data usage, expressed in kbps. If this is zero then no limit is applied.
33
+ #
34
+ # @return [Float]
35
+ attr_reader :requested_maximum_bandwidth
36
+
32
37
  # Initializes this new Lightstreamer session with the passed options.
33
38
  #
34
39
  # @param [Hash] options The options to create the session with.
@@ -36,6 +41,8 @@ module Lightstreamer
36
41
  # @option options [String] :username The username to connect to the server with.
37
42
  # @option options [String] :password The password to connect to the server with.
38
43
  # @option options [String] :adapter_set The name of the adapter set to request from the server.
44
+ # @option options [Float] :requested_maximum_bandwidth. The server-side bandwidth constraint on data usage,
45
+ # expressed in kbps. Defaults to zero which means no limit is applied.
39
46
  def initialize(options = {})
40
47
  @subscriptions = []
41
48
  @subscriptions_mutex = Mutex.new
@@ -44,41 +51,55 @@ module Lightstreamer
44
51
  @username = options[:username]
45
52
  @password = options[:password]
46
53
  @adapter_set = options[:adapter_set]
54
+ @requested_maximum_bandwidth = options[:requested_maximum_bandwidth].to_f
55
+
56
+ @on_message_result_callbacks = []
47
57
  end
48
58
 
49
- # Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
59
+ # Connects a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
50
60
  # a {LightstreamerError} subclass will be raised.
51
61
  def connect
52
62
  return if @stream_connection
53
63
 
54
64
  @error = nil
55
65
 
56
- create_stream_connection
57
- create_control_connection
66
+ @stream_connection = StreamConnection.new self
67
+ @stream_connection.connect
68
+
58
69
  create_processing_thread
59
70
  rescue
60
71
  @stream_connection = nil
61
72
  raise
62
73
  end
63
74
 
64
- # Returns whether this session is currently connected and has an active stream connection.
75
+ # Returns whether this Lightstreamer session is currently connected and has an active stream connection.
65
76
  #
66
77
  # @return [Boolean]
67
78
  def connected?
68
79
  !@stream_connection.nil?
69
80
  end
70
81
 
71
- # Disconnects this session and terminates the session on the server. All worker threads are exited.
82
+ # Returns the ID of the currently active Lightstreamer session, or `nil` if there is no active session.
83
+ #
84
+ # @return [String, nil]
85
+ def session_id
86
+ @stream_connection && @stream_connection.session_id
87
+ end
88
+
89
+ # Disconnects this Lightstreamer session and terminates the session on the server. All worker threads are exited.
72
90
  def disconnect
73
- @control_connection.execute :destroy if @control_connection
91
+ control_request :destroy if @stream_connection
74
92
 
75
93
  @processing_thread.join 5 if @processing_thread
76
94
  ensure
77
95
  @stream_connection.disconnect if @stream_connection
78
96
  @processing_thread.exit if @processing_thread
79
97
 
80
- @processing_thread = @control_connection = @stream_connection = nil
81
- @subscriptions = []
98
+ @subscriptions.each do |subscription|
99
+ subscription.instance_variable_set :@active, false
100
+ end
101
+
102
+ @processing_thread = @stream_connection = nil
82
103
  end
83
104
 
84
105
  # Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
@@ -88,66 +109,123 @@ module Lightstreamer
88
109
  def force_rebind
89
110
  return unless @stream_connection
90
111
 
91
- @control_connection.execute :force_rebind
112
+ control_request :force_rebind
92
113
  end
93
114
 
94
- # Subscribes this Lightstreamer session to the specified subscription. If an error occurs then a
95
- # {LightstreamerError} subclass will be raised.
115
+ # Builds a new subscription for this session with the specified options. Note that ths does not activate the
116
+ # subscription, {Subscription#start} must be called to actually start streaming the subscription's data. See the
117
+ # {Subscription} class for more details.
118
+ #
119
+ # @param [Hash] options The options to create the subscription with.
120
+ # @option options [Array] :items The names of the items to subscribe to. Required.
121
+ # @option options [Array] :fields The names of the fields to subscribe to on the items. Required.
122
+ # @option options [:distinct, :merge] :mode The operation mode of the subscription. Required.
123
+ # @option options [String] :adapter The name of the data adapter from this session's adapter set that should be
124
+ # used. If `nil` then the default data adapter will be used.
125
+ # @option options [String] :selector The selector for table items. Optional.
126
+ # @option options [Float, :unfiltered] :maximum_update_frequency The maximum number of updates the subscription
127
+ # should receive per second. Defaults to zero which means there is no limit on the update frequency.
128
+ # If set to `:unfiltered` then unfiltered streaming will be used for the subscription and it is
129
+ # possible for overflows to occur (see {Subscription#on_overflow}).
96
130
  #
97
- # @param [Subscription] subscription The new subscription to subscribe to.
98
- def subscribe(subscription)
99
- subscription.clear_data
131
+ # @return [Subscription] The new subscription.
132
+ def build_subscription(options)
133
+ subscription = Subscription.new self, options
100
134
 
101
135
  @subscriptions_mutex.synchronize { @subscriptions << subscription }
102
136
 
103
- options = { mode: subscription.mode, items: subscription.items, fields: subscription.fields,
104
- adapter: subscription.adapter, maximum_update_frequency: subscription.maximum_update_frequency,
105
- selector: subscription.selector }
137
+ subscription
138
+ end
139
+
140
+ # Stops the specified subscription and removes it from this session. If an error occurs then a {LightstreamerError}
141
+ # subclass will be raised. To just stop a subscription with the option of restarting it at a later date call
142
+ # {Subscription#stop} on the subscription itself.
143
+ def remove_subscription(subscription)
144
+ subscription.stop
106
145
 
107
- @control_connection.subscription_execute :add, subscription.id, options
108
- rescue
109
146
  @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
110
- raise
111
147
  end
112
148
 
113
- # Returns whether the specified subscription is currently active on this session.
114
- #
115
- # @param [Subscription] subscription The subscription to return the status for.
149
+ # This method performs a bulk {Subscription#start} on all the passed subscriptions. Calling {Subscription#start} on
150
+ # each of them individually would also work but requires a separate POST request to be sent for every subscription.
151
+ # This request starts all of the passed subscriptions in a single POST request which is significantly faster for
152
+ # a large number of subscriptions. The return value is an array with one entry per subscription and indicates the
153
+ # error state returned by the server for that subscription's start request, or `nil` if no error occurred.
116
154
  #
117
- # @return [Boolean] Whether the specified subscription is currently active on this session.
118
- def subscribed?(subscription)
119
- @subscriptions_mutex.synchronize { @subscriptions.include? subscription }
120
- end
121
-
122
- # Unsubscribes this Lightstreamer session from the specified subscription.
155
+ # @param [Array<Subscription>] subscriptions The subscriptions to start.
123
156
  #
124
- # @param [Subscription] subscription The existing subscription to unsubscribe from.
125
- def unsubscribe(subscription)
126
- raise ArgumentError, 'Unknown subscription' unless subscribed? subscription
157
+ # @return [Array<LightstreamerError, nil>]
158
+ def bulk_subscription_start(*subscriptions)
159
+ request_bodies = subscriptions.map do |subscription|
160
+ PostRequest.request_body session_id, *subscription.start_control_request_args
161
+ end
127
162
 
128
- @control_connection.subscription_execute :delete, subscription.id
163
+ errors = PostRequest.bulk_execute @stream_connection.control_address, request_bodies
129
164
 
130
- @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
165
+ # Set @active to true on all subscriptions that did not have an error
166
+ errors.each_with_index do |error, index|
167
+ subscriptions[index].instance_variable_set :@active, true if error.nil?
168
+ end
131
169
  end
132
170
 
133
- private
171
+ # Sets the server-side bandwidth constraint on data usage for this session, expressed in kbps. A value of zero
172
+ # means no limit will be applied. If an error occurs then a {LightstreamerError} subclass will be raised.
173
+ #
174
+ # @param [Float] bandwidth The new requested maximum bandwidth, expressed in kbps.
175
+ def requested_maximum_bandwidth=(bandwidth)
176
+ control_request :constrain, LS_requested_max_bandwidth: bandwidth if connected?
177
+ @requested_maximum_bandwidth = bandwidth.to_f
178
+ end
134
179
 
135
- def create_stream_connection
136
- @stream_connection = StreamConnection.new self
137
- @stream_connection.connect
180
+ # Sends a custom message to the Lightstreamer server. Message sending can be done synchronously or asynchronously.
181
+ # By default the message will be sent synchronously, i.e. the message will be processed by the server and if an
182
+ # error occurs a {LightstreamerError} subclass will be raised immediately. However, if the `:async` option is true
183
+ # then the message will be sent asynchronously, and the result of the message send will be reported to all callbacks
184
+ # that have been registered via {#on_message_result}.
185
+ #
186
+ # @param [String] message The message to send to the Lightstreamer server.
187
+ # @param [Hash] options The options that control messages sent asynchronously.
188
+ # @option options [Boolean] :async Whether to send the message asynchronously. Defaults to `false`.
189
+ # @option options [String] :sequence The alphanumeric identifier that identifies a subset of messages that are to
190
+ # be processed in sequence based on the `:number` given to them. If the special `UNORDERED_MESSAGES`
191
+ # sequence is used then the associated messages are processed immediately, possibly concurrently,
192
+ # with no ordering constraint.
193
+ # @option options [Fixnum] :number The progressive number of this message within its sequence. Should start at 1.
194
+ # @option options [Float] :max_wait The maximum time the server can wait before processing this message if one or
195
+ # more of the preceding messages in the same sequence have not been received. If not specified then
196
+ # a timeout is assigned by the server.
197
+ def send_message(message, options = {})
198
+ url = URI.join(@stream_connection.control_address, '/lightstreamer/send_message.txt').to_s
199
+
200
+ query = { LS_session: session_id, LS_message: message }
201
+ query[:LS_sequence] = options.fetch(:sequence) if options[:async]
202
+ query[:LS_msg_prog] = options.fetch(:number) if options[:async]
203
+ query[:LS_max_wait] = options[:max_wait] if options[:max_wait]
204
+
205
+ PostRequest.execute url, query
138
206
  end
139
207
 
140
- def create_control_connection
141
- control_address = @stream_connection.control_address || server_url
208
+ # Adds the passed block to the list of callbacks that will be run when the outcome of an asynchronous message send
209
+ # arrives. The block will be called on a worker thread and so the code that is run by the block must be thread-safe.
210
+ # The arguments passed to the block are `|sequence, numbers, error|`.
211
+ #
212
+ # @param [Proc] callback The callback that is to be run.
213
+ def on_message_result(&callback)
214
+ @on_message_result_callbacks << callback
215
+ end
142
216
 
143
- # If the control address doesn't have a schema then use the same schema as the server URL
144
- unless control_address.start_with? 'http'
145
- control_address = "#{URI(server_url).scheme}://#{control_address}"
146
- end
217
+ # Sends a request to the control connection. If an error occurs then a {LightstreamerError} subclass will be raised.
218
+ #
219
+ # @param [Symbol] operation The control operation to perform.
220
+ # @param [Hash] options The options to send with the control request.
221
+ def control_request(operation, options = {})
222
+ url = URI.join(@stream_connection.control_address, '/lightstreamer/control.txt').to_s
147
223
 
148
- @control_connection = ControlConnection.new @stream_connection.session_id, control_address
224
+ PostRequest.execute url, options.merge(LS_session: session_id, LS_op: operation)
149
225
  end
150
226
 
227
+ private
228
+
151
229
  # Starts the processing thread that reads and processes incoming data from the stream connection.
152
230
  def create_processing_thread
153
231
  @processing_thread = Thread.new do
@@ -155,28 +233,37 @@ module Lightstreamer
155
233
 
156
234
  loop do
157
235
  line = @stream_connection.read_line
158
-
159
236
  break if line.nil?
160
237
 
161
- process_stream_data line unless line.empty?
238
+ process_stream_line line
162
239
  end
163
240
 
164
241
  # The stream connection has terminated so the session is assumed to be over
165
242
  @error = @stream_connection.error
166
- @processing_thread = @control_connection = @stream_connection = nil
243
+ @processing_thread = @stream_connection = nil
167
244
  end
168
245
  end
169
246
 
170
- # Processes a single line of incoming stream data by passing it to all the active subscriptions until one
247
+ # Processes a single line of incoming stream data by passing it to all the subscriptions until one
171
248
  # successfully processes it. This method is always run on the processing thread.
172
- def process_stream_data(line)
173
- was_processed = @subscriptions_mutex.synchronize do
174
- @subscriptions.detect do |subscription|
175
- subscription.process_stream_data line
176
- end
249
+ def process_stream_line(line)
250
+ return if @subscriptions.any? { |subscription| subscription.process_stream_data line }
251
+ return if process_send_message_outcome line
252
+
253
+ warn "Lightstreamer: unprocessed stream data '#{line}'"
254
+ end
255
+
256
+ # Attempts to process the passed line as a send message outcome message, and if is such a message then the
257
+ # registered callbacks are run.
258
+ def process_send_message_outcome(line)
259
+ outcome = SendMessageOutcomeMessage.parse line
260
+ return unless outcome
261
+
262
+ @on_message_result_callbacks.each do |callback|
263
+ callback.call outcome.sequence, outcome.numbers, outcome.error
177
264
  end
178
265
 
179
- warn "Lightstreamer: unprocessed stream data '#{line}'" unless was_processed
266
+ true
180
267
  end
181
268
  end
182
269
  end
@@ -3,7 +3,7 @@ module Lightstreamer
3
3
  # as they become complete.
4
4
  #
5
5
  # @private
6
- class LineBuffer
6
+ class StreamBuffer
7
7
  def initialize
8
8
  @buffer = ''
9
9
  end
@@ -9,7 +9,7 @@ module Lightstreamer
9
9
  # @return [String, nil]
10
10
  attr_reader :session_id
11
11
 
12
- # The control address returned from the server when this stream connection was initiated.
12
+ # The control address to use for this stream connection.
13
13
  #
14
14
  # @return [String, nil]
15
15
  attr_reader :control_address
@@ -27,9 +27,6 @@ module Lightstreamer
27
27
  @session = session
28
28
  @queue = Queue.new
29
29
 
30
- @stream_create_url = URI.join(session.server_url, '/lightstreamer/create_session.txt').to_s
31
- @stream_bind_url = URI.join(session.server_url, '/lightstreamer/bind_session.txt').to_s
32
-
33
30
  @connect_result_mutex = Mutex.new
34
31
  @connect_result_condition_variable = ConditionVariable.new
35
32
  end
@@ -99,26 +96,29 @@ module Lightstreamer
99
96
 
100
97
  def create_new_stream
101
98
  params = { LS_op2: 'create', LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg', LS_user: @session.username,
102
- LS_password: @session.password }
99
+ LS_password: @session.password, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
103
100
 
104
101
  params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
105
102
 
106
- execute_stream_post_request @stream_create_url, connect_timeout: 15, query: params
103
+ url = URI.join(@session.server_url, '/lightstreamer/create_session.txt').to_s
104
+ execute_stream_post_request url, connect_timeout: 15, query: params
107
105
 
108
106
  signal_connect_result_ready
109
107
  end
110
108
 
111
109
  def bind_to_existing_stream
112
- execute_stream_post_request @stream_bind_url, connect_timeout: 15, query: { LS_session: @session_id }
110
+ params = { LS_session: @session_id, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
111
+
112
+ url = URI.join(control_address, '/lightstreamer/bind_session.txt').to_s
113
+ execute_stream_post_request url, connect_timeout: 15, query: params
113
114
  end
114
115
 
115
116
  def execute_stream_post_request(url, options)
116
117
  @header = StreamConnectionHeader.new
117
118
 
118
- buffer = LineBuffer.new
119
- options[:response_block] = lambda do |data, _remaining_bytes, _total_bytes|
120
- buffer.process data, &method(:process_stream_line)
121
- end
119
+ buffer = StreamBuffer.new
120
+ options[:response_block] = -> (data, _, _) { buffer.process data, &method(:process_stream_line) }
121
+ options[:expects] = 200
122
122
 
123
123
  Excon.post url, options
124
124
  rescue Excon::Error => error
@@ -141,7 +141,13 @@ module Lightstreamer
141
141
  header_incomplete = @header.process_line line
142
142
 
143
143
  @session_id = @header['SessionId']
144
- @control_address = @header['ControlAddress']
144
+
145
+ # Set the control address and ensure it has a schema
146
+ @control_address = @header['ControlAddress'] || @session.server_url
147
+ unless @control_address.start_with? 'http'
148
+ @control_address = "#{URI(@session.server_url).scheme}://#{@control_address}"
149
+ end
150
+
145
151
  @error = @header.error
146
152
 
147
153
  return if header_incomplete
@@ -1,13 +1,13 @@
1
1
  module Lightstreamer
2
- # Describes a subscription that can be bound to a {Session} in order to consume its streaming data. A subscription is
3
- # described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous data
4
- # callback using {#on_data} or by polling using {#item_data}. Subscriptions start receiving data once they are
5
- # attached to a session using {Session#subscribe}.
2
+ # This class manages a subscription that can be used to stream data from a {Session}. Subscriptions should always be
3
+ # created using {Session#build_subscription}. Subscriptions start receiving data after {#start} is called, and
4
+ # streaming subscription data can be consumed by registering an asynchronous data callback using {#on_data}, or by
5
+ # polling using {#item_data}.
6
6
  class Subscription
7
- # The unique identification number of this subscription.
7
+ # The session that this subscription is associated with.
8
8
  #
9
- # @return [Fixnum]
10
- attr_reader :id
9
+ # @return [Session]
10
+ attr_reader :session
11
11
 
12
12
  # The names of the items to subscribe to.
13
13
  #
@@ -42,29 +42,26 @@ module Lightstreamer
42
42
  # @return [Float, :unfiltered]
43
43
  attr_reader :maximum_update_frequency
44
44
 
45
- # Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
46
- # {Session#subscribe} to activate the subscription on a Lightstreamer session.
47
- #
48
- # @param [Hash] options The options to create the subscription with.
49
- # @option options [Array] :items The names of the items to subscribe to. Required.
50
- # @option options [Array] :fields The names of the fields to subscribe to on the items. Required.
51
- # @option options [:distinct, :merge] :mode The operation mode of this subscription. Required.
52
- # @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
53
- # should be used. If `nil` then the default data adapter will be used.
54
- # @option options [String] :selector The selector for table items. Optional.
55
- # @option options [Float, :unfiltered] :maximum_update_frequency The maximum number of updates this subscription
56
- # should receive per second. Defaults to zero which means there is no limit on the update frequency.
57
- # If set to `:unfiltered` then unfiltered streaming will be used for this subscription and it is
58
- # possible for overflows to occur (see {#on_overflow}).
59
- def initialize(options)
60
- @id = self.class.next_id
45
+ # Whether this subscription is currently started and actively streaming data. See {#start} and {#stop} for details.
46
+ #
47
+ # @return [Boolean]
48
+ attr_reader :active
49
+
50
+ # Initializes a new Lightstreamer subscription with the specified options.
51
+ #
52
+ # @param [Session] session The session this subscription is associated with.
53
+ # @param [Hash] options The options to create the subscription with. See {Session#build_subscription}
54
+ #
55
+ # @private
56
+ def initialize(session, options)
57
+ @session = session
61
58
 
62
59
  @items = options.fetch(:items)
63
60
  @fields = options.fetch(:fields)
64
61
  @mode = options.fetch(:mode).to_sym
65
62
  @adapter = options[:adapter]
66
63
  @selector = options[:selector]
67
- @maximum_update_frequency = options[:maximum_update_frequency] || 0.0
64
+ @maximum_update_frequency = sanitize_frequency options[:maximum_update_frequency]
68
65
 
69
66
  @data_mutex = Mutex.new
70
67
 
@@ -72,6 +69,76 @@ module Lightstreamer
72
69
  clear_callbacks
73
70
  end
74
71
 
72
+ # Returns this subscription's unique identification number.
73
+ #
74
+ # @return [Fixnum]
75
+ #
76
+ # @private
77
+ def id
78
+ @id ||= ID_GENERATOR.next
79
+ end
80
+
81
+ # Starts streaming data for this Lightstreamer subscription. If an error occurs then a {LightstreamerError} subclass
82
+ # will be raised.
83
+ #
84
+ # @param [Hash] options The options to start the subscription with.
85
+ # @option options [Boolean] :silent Whether the subscription should be started in silent mode. In silent mode the
86
+ # subscription is initiated on the server and begins buffering incoming data, however this data will
87
+ # not be sent to the client for processing until {#unsilence} is called.
88
+ # @option options [Boolean, Fixnum] :snapshot Controls whether the server should send a snapshot of this
89
+ # subscription's items. If `false` then the server does not send snapshot information (this is the
90
+ # default). If `true` then the server will send snapshot information if it's available. If this
91
+ # subscription's {#mode} is `:distinct` then `:snapshot` can also be an integer specifying the
92
+ # number of events the server should send as part of the snapshot. If this latter option is used
93
+ # then any callbacks registered with {#on_end_of_snapshot} will be called once the snapshot for each
94
+ # item is complete.
95
+ def start(options = {})
96
+ session.control_request(*start_control_request_args(options)) unless @active
97
+ @active = true
98
+ end
99
+
100
+ # Returns the arguments to pass to to {Session#control_request} in order ot start this subscription with the given
101
+ # options.
102
+ #
103
+ # @param [Hash] options The options to start the subscription with.
104
+ #
105
+ # @private
106
+ def start_control_request_args(options = {})
107
+ operation = options[:silent] ? :add_silent : :add
108
+
109
+ options = { LS_table: id, LS_mode: mode.to_s.upcase, LS_id: items, LS_schema: fields, LS_data_adapter: adapter,
110
+ LS_requested_max_frequency: maximum_update_frequency, LS_selector: selector,
111
+ LS_snapshot: options.fetch(:snapshot, false) }
112
+
113
+ [operation, options]
114
+ end
115
+
116
+ # Unsilences this subscription if it was initially started in silent mode (by passing `silent: true` to {#start}).
117
+ # If this subscription was not started in silent mode then this method has no effect. If an error occurs then a
118
+ # {LightstreamerError} subclass will be raised.
119
+ def unsilence
120
+ session.control_request :start, LS_table: id
121
+ end
122
+
123
+ # Stops streaming data for this Lightstreamer subscription. If an error occurs then a {LightstreamerError} subclass
124
+ # will be raised.
125
+ def stop
126
+ session.control_request :delete, LS_table: id if @active
127
+ @active = false
128
+ end
129
+
130
+ # Sets this subscription's maximum update frequency. This can be done while a subscription is streaming data in
131
+ # order to change its update frequency limit, but an actively streaming subscription cannot switch between filtered
132
+ # and unfiltered dispatching, and {TableModificationNotAllowedError} will be raised if this is attempted.
133
+ #
134
+ # @param [Float, :unfiltered] new_frequency The new maximum update frequency. See {#maximum_update_frequency} for
135
+ # details.
136
+ def maximum_update_frequency=(new_frequency)
137
+ new_frequency = sanitize_frequency new_frequency
138
+ session.control_request :reconf, LS_table: id, LS_requested_max_frequency: new_frequency if @active
139
+ @maximum_update_frequency = new_frequency
140
+ end
141
+
75
142
  # Clears all current data stored for this subscription. New data will continue to be processed as it becomes
76
143
  # available.
77
144
  def clear_data
@@ -85,9 +152,7 @@ module Lightstreamer
85
152
  #
86
153
  # @param [Proc] callback The callback that is to be run when new data arrives.
87
154
  def on_data(&callback)
88
- @data_mutex.synchronize do
89
- @callbacks[:on_data] << callback
90
- end
155
+ @data_mutex.synchronize { @callbacks[:on_data] << callback }
91
156
  end
92
157
 
93
158
  # Adds the passed block to the list of callbacks that will be run when the server reports an overflow for this
@@ -96,16 +161,21 @@ module Lightstreamer
96
161
  #
97
162
  # @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
98
163
  def on_overflow(&callback)
99
- @data_mutex.synchronize do
100
- @callbacks[:on_overflow] << callback
101
- end
164
+ @data_mutex.synchronize { @callbacks[:on_overflow] << callback }
165
+ end
166
+
167
+ # Adds the passed block to the list of callbacks that will be run when the server reports an end-of-snapshot
168
+ # notification for this subscription. The block will be called on a worker thread and so the code that is run by the
169
+ # block must be thread-safe. The arguments passed to the block are `|subscription, item_name|`.
170
+ #
171
+ # @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
172
+ def on_end_of_snapshot(&callback)
173
+ @data_mutex.synchronize { @callbacks[:on_end_of_snapshot] << callback }
102
174
  end
103
175
 
104
176
  # Removes all {#on_data} and {#on_overflow} callbacks present on this subscription.
105
177
  def clear_callbacks
106
- @data_mutex.synchronize do
107
- @callbacks = { on_data: [], on_overflow: [] }
108
- end
178
+ @data_mutex.synchronize { @callbacks = { on_data: [], on_overflow: [], on_end_of_snapshot: [] } }
109
179
  end
110
180
 
111
181
  # Returns a copy of the current data of one of this subscription's items.
@@ -117,9 +187,20 @@ module Lightstreamer
117
187
  index = @items.index item_name
118
188
  raise ArgumentError, 'Unknown item' unless index
119
189
 
120
- @data_mutex.synchronize do
121
- @data[index].dup
122
- end
190
+ @data_mutex.synchronize { @data[index].dup }
191
+ end
192
+
193
+ # Sets the current data for the item with the specified name.
194
+ #
195
+ # @param [String] item_name The name of the item to set the data for.
196
+ # @param [Hash] item_data The new data for the item.
197
+ def set_item_data(item_name, item_data)
198
+ index = @items.index item_name
199
+ raise ArgumentError, 'Unknown item' unless index
200
+
201
+ raise ArgumentError, 'Item data must be a hash' unless item_data.is_a? Hash
202
+
203
+ @data_mutex.synchronize { @data[index] = item_data.dup }
123
204
  end
124
205
 
125
206
  # Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
@@ -132,30 +213,23 @@ module Lightstreamer
132
213
  #
133
214
  # @private
134
215
  def process_stream_data(line)
135
- process_update_message(line) || process_overflow_message(line)
136
- end
137
-
138
- # Returns the next unique subscription ID.
139
- #
140
- # @return [Fixnum]
141
- #
142
- # @private
143
- def self.next_id
144
- @next_id ||= 0
145
- @next_id += 1
216
+ return true if process_update_message UpdateMessage.parse(line, id, items, fields)
217
+ return true if process_overflow_message OverflowMessage.parse(line, id, items)
218
+ return true if process_end_of_snapshot_message EndOfSnapshotMessage.parse(line, id, items)
146
219
  end
147
220
 
148
221
  private
149
222
 
150
- def process_update_message(line)
151
- update_message = UpdateMessage.parse line, id, items, fields
152
- return unless update_message
223
+ ID_GENERATOR = (1..Float::INFINITY).each
153
224
 
154
- @data_mutex.synchronize do
155
- process_new_values update_message.item_index, update_message.values
156
- end
225
+ def sanitize_frequency(frequency)
226
+ frequency.to_s == 'unfiltered' ? :unfiltered : frequency.to_f
227
+ end
157
228
 
158
- true
229
+ def process_update_message(message)
230
+ return unless message
231
+
232
+ @data_mutex.synchronize { process_new_values message.item_index, message.values }
159
233
  end
160
234
 
161
235
  def process_new_values(item_index, new_values)
@@ -167,21 +241,22 @@ module Lightstreamer
167
241
  run_callbacks :on_data, @items[item_index], data, new_values
168
242
  end
169
243
 
170
- def process_overflow_message(line)
171
- overflow_message = OverflowMessage.parse line, id, items
172
- return unless overflow_message
244
+ def process_overflow_message(message)
245
+ return unless message
173
246
 
174
- @data_mutex.synchronize do
175
- run_callbacks :on_overflow, @items[overflow_message.item_index], overflow_message.overflow_size
176
- end
247
+ @data_mutex.synchronize { run_callbacks :on_overflow, @items[message.item_index], message.overflow_size }
248
+ end
177
249
 
178
- true
250
+ def process_end_of_snapshot_message(message)
251
+ return unless message
252
+
253
+ @data_mutex.synchronize { run_callbacks :on_end_of_snapshot, @items[message.item_index] }
179
254
  end
180
255
 
181
- def run_callbacks(callback_type, *arguments)
182
- @callbacks.fetch(callback_type).each do |callback|
183
- callback.call self, *arguments
184
- end
256
+ def run_callbacks(callback_type, *args)
257
+ @callbacks.fetch(callback_type).each { |callback| callback.call self, *args }
258
+
259
+ true
185
260
  end
186
261
  end
187
262
  end
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.6'.freeze
3
+ VERSION = '0.7'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lightstreamer
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.6'
4
+ version: '0.7'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Viney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-27 00:00:00.000000000 Z
11
+ date: 2016-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0.41'
145
+ version: '0.42'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0.41'
152
+ version: '0.42'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: yard
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -178,12 +178,14 @@ files:
178
178
  - lib/lightstreamer.rb
179
179
  - lib/lightstreamer/cli/commands/stream_command.rb
180
180
  - lib/lightstreamer/cli/main.rb
181
- - lib/lightstreamer/control_connection.rb
182
181
  - lib/lightstreamer/errors.rb
183
- - lib/lightstreamer/line_buffer.rb
182
+ - lib/lightstreamer/messages/end_of_snapshot_message.rb
184
183
  - lib/lightstreamer/messages/overflow_message.rb
184
+ - lib/lightstreamer/messages/send_message_outcome_message.rb
185
185
  - lib/lightstreamer/messages/update_message.rb
186
+ - lib/lightstreamer/post_request.rb
186
187
  - lib/lightstreamer/session.rb
188
+ - lib/lightstreamer/stream_buffer.rb
187
189
  - lib/lightstreamer/stream_connection.rb
188
190
  - lib/lightstreamer/stream_connection_header.rb
189
191
  - lib/lightstreamer/subscription.rb
@@ -1,106 +0,0 @@
1
- module Lightstreamer
2
- # Helper class used by {Session} and is responsible for sending Lightstreamer control requests.
3
- #
4
- # @private
5
- class ControlConnection
6
- # Initializes this class for sending Lightstreamer control requests using the specified session ID and control
7
- # address.
8
- #
9
- # @param [String] session_id The Lightstreamer session ID.
10
- # @param [String] control_url The URL of the server to send Lightstreamer control requests to.
11
- def initialize(session_id, control_url)
12
- @session_id = session_id
13
- @control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
14
- end
15
-
16
- # Sends a Lightstreamer control request that executes the specified operation with the specified options. If an
17
- # error occurs then a {LightstreamerError} subclass will be raised.
18
- #
19
- # @param [String] operation The operation to execute.
20
- # @param [Hash] options The options to include on the request.
21
- def execute(operation, options = {})
22
- result = execute_post_request build_payload(operation, options)
23
-
24
- raise Errors::SyncError if result.first == 'SYNC ERROR'
25
- raise LightstreamerError.build(result[2], result[1]) if result.first != 'OK'
26
- end
27
-
28
- # Sends a Lightstreamer subscription control request with the specified operation, table, and options. If an error
29
- # occurs then a {LightstreamerError} subclass will be raised.
30
- #
31
- # @param [:add, :add_silent, :start, :delete] operation The operation to execute.
32
- # @param [Fixnum] table The ID of the table this request pertains to.
33
- # @param [Hash] options The subscription control request options.
34
- def subscription_execute(operation, table, options = {})
35
- options[:table] = table
36
-
37
- validate_subscription_options operation, options
38
-
39
- execute operation, options
40
- end
41
-
42
- private
43
-
44
- # Validates the passed subscription control request options.
45
- def validate_subscription_options(operation, options)
46
- raise ArgumentError, 'Invalid table' unless options[:table].is_a? Fixnum
47
- raise ArgumentError, 'Unsupported operation' unless [:add, :add_silent, :start, :delete].include? operation
48
-
49
- validate_add_subscription_options options if [:add, :add_silent].include? operation
50
- end
51
-
52
- # Validates options required for subscription control requests that perform `add` operations.
53
- def validate_add_subscription_options(options)
54
- raise ArgumentError, 'Items not specified' if Array(options[:items]).empty?
55
- raise ArgumentError, 'Fields not specified' if Array(options[:fields]).empty?
56
- raise ArgumentError, 'Unsupported mode' unless [:distinct, :merge].include? options[:mode]
57
- end
58
-
59
- # Executes a POST request to the control address with the specified payload. Raises {ConnectionError} if the HTTP
60
- # request fails. Returns the response body split into individual lines.
61
- def execute_post_request(payload)
62
- response = Excon.post @control_url, body: URI.encode_www_form(payload), connect_timeout: 15
63
-
64
- response.body.split("\n").map(&:strip)
65
- rescue Excon::Error => error
66
- raise Errors::ConnectionError, error.message
67
- end
68
-
69
- # Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
70
- # details on the supported keys.
71
- def build_payload(operation, options)
72
- params = {}
73
-
74
- build_optional_payload_fields options, params
75
-
76
- params[:LS_session] = @session_id
77
- params[:LS_op] = operation
78
-
79
- params
80
- end
81
-
82
- OPTION_NAME_TO_API_PARAMETER = {
83
- table: :LS_table,
84
- adapter: :LS_data_adapter,
85
- items: :LS_id,
86
- fields: :LS_schema,
87
- selector: :LS_selector,
88
- maximum_update_frequency: :LS_requested_max_frequency
89
- }.freeze
90
-
91
- def build_optional_payload_fields(options, params)
92
- params[:LS_mode] = options[:mode].to_s.upcase if options[:mode]
93
-
94
- options.each do |key, value|
95
- next if key == :mode
96
- next if value.nil?
97
-
98
- value = value.map(&:to_s).join(' ') if value.is_a? Array
99
-
100
- api_parameter = OPTION_NAME_TO_API_PARAMETER.fetch key
101
-
102
- params[api_parameter] = value
103
- end
104
- end
105
- end
106
- end