lightstreamer 0.6 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
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