lightstreamer 0.3 → 0.4

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: f96a70c42b710644985ba440779ec2d73f32ad9e
4
- data.tar.gz: 4c0181a0612a67870d3333671ecbb20e0af27f8c
3
+ metadata.gz: 863ad0ba9936b00dca802004782358a03cf5cd36
4
+ data.tar.gz: a13adc3e4b6b9945580bc02927826d61742dba1d
5
5
  SHA512:
6
- metadata.gz: 5048265eaf80652222323ffad4f4fa4a39343c4adbc8a52ae5b449d43a8c63e343334cab654a2f6fdc3321955b418581661e0c596e23f1f3ea83f953e9a77f02
7
- data.tar.gz: 248b20766df771c73189fd3eb61f218a5b016254a2271f965c4076cfde2bd9391eef4ab6afcc1ea5a5424a3ca86c4f15728aeb12bf415bb2b03721722828bced
6
+ metadata.gz: 7f9dd91cc66467027ddd209d0c964559db461e8b9a05c1cb9aa583ae42d1997516c0196ea2be18c0fbc5d317db97fcef10ce81e32cefe677390dd4c8f339ebfc
7
+ data.tar.gz: 4863842643a0d7a876f6efa2b41270044f7ada1479c0154e452e02f7e1d6b2e75c39f6f24d79029b633dd7402c3cd6a5d24a41e5c3dba057013a0ddc403b9dbc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Lightstreamer Changelog
2
2
 
3
+ ### 0.4 — July 25, 2016
4
+
5
+ - Added support for specifying a subscription's selector and maximum update frequency
6
+ - Added `Lightstreamer::Session#force_rebind` which asks the server to send a `LOOP` message so the client has to rebind
7
+ using a new stream connection
8
+ - Added validation of the arguments for control connection subscription requests
9
+ - All error classes now inherit from `Lightstreamer::Error` and the different Lightstreamer errors that can occur are
10
+ separated into a variety of new subclasses
11
+ - Removed `Lightstreamer::ProtocolError`
12
+ - `Lightstreamer::Session#disconnect` now properly terminates the session with the server by sending the relevant
13
+ control request
14
+ - The reason why a session terminated can now be queried using `Lightstreamer::Session#error`
15
+ - Fixed handling of `nil` subscription adapters
16
+ - Correctly handle when an `END` message is received on the stream connection
17
+ - All valid error responses from session create and bind requests are now handled correctly
18
+ - Unhandled exceptions on the internal worker threads now cause the application to terminate
19
+
3
20
  ### 0.3 — July 24, 2016
4
21
 
5
22
  - Seamlessly rebind the stream connection when a `LOOP` message is received
data/lib/lightstreamer.rb CHANGED
@@ -3,9 +3,8 @@ require 'typhoeus'
3
3
  require 'uri'
4
4
 
5
5
  require 'lightstreamer/control_connection'
6
+ require 'lightstreamer/errors'
6
7
  require 'lightstreamer/line_buffer'
7
- require 'lightstreamer/protocol_error'
8
- require 'lightstreamer/request_error'
9
8
  require 'lightstreamer/session'
10
9
  require 'lightstreamer/stream_connection'
11
10
  require 'lightstreamer/stream_connection_header'
@@ -12,6 +12,8 @@ module Lightstreamer
12
12
  option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
13
13
  option :fields, type: :array, required: true, desc: 'The field(s) to stream'
14
14
  option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
15
+ option :selector, desc: 'The selector for table items'
16
+ option :maximum_update_frequency, type: :numeric, desc: 'The maximum number of updates per second for each item'
15
17
 
16
18
  def stream
17
19
  session = create_session
@@ -36,14 +38,20 @@ module Lightstreamer
36
38
 
37
39
  # Creates a new subscription from the specified options.
38
40
  def create_subscription
39
- subscription = Lightstreamer::Subscription.new items: options[:items], fields: options[:fields],
40
- mode: options[:mode], adapter: options[:adapter]
41
+ subscription = Lightstreamer::Subscription.new subscription_options
41
42
 
42
43
  subscription.add_data_callback(&method(:subscription_data_callback))
43
44
 
44
45
  subscription
45
46
  end
46
47
 
48
+ def subscription_options
49
+ {
50
+ items: options[:items], fields: options[:fields], mode: options[:mode], adapter: options[:adapter],
51
+ maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector]
52
+ }
53
+ end
54
+
47
55
  def subscription_data_callback(_subscription, item_name, _item_data, new_values)
48
56
  @queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
49
57
  end
@@ -11,26 +11,56 @@ module Lightstreamer
11
11
  @control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
12
12
  end
13
13
 
14
- # Sends a Lightstreamer control request with the specified options. If an error occurs then {RequestError} or
15
- # {ProtocolError} will be raised.
14
+ # Sends a Lightstreamer control request that executes the specified operation with the specified options. If an
15
+ # error occurs then an {Error} subclass will be raised.
16
16
  #
17
- # @param [Hash] options The control request options.
18
- # @option options [Fixnum] :table The ID of the table this request pertains to. Required.
19
- # @option options [:add, :add_silent, :start, :delete] :operation The operation to perform. Required.
17
+ # @param [String] operation The operation to execute.
18
+ # @param [Hash] options The options to include on the request.
19
+ def execute(operation, options = {})
20
+ result = execute_post_request build_payload(operation, options)
21
+
22
+ raise Error.build(result[2], result[1]) if result.first != 'OK'
23
+ end
24
+
25
+ # Sends a Lightstreamer subscription control request with the specified operation, table, and options. If an error
26
+ # occurs then an {Error} subclass will be raised.
27
+ #
28
+ # @param [:add, :add_silent, :start, :delete] operation The operation to execute.
29
+ # @param [Fixnum] table The ID of the table this request pertains to.
30
+ # @param [Hash] options The subscription control request options.
20
31
  # @option options [String] :adapter The name of the data adapter to use. Optional.
21
32
  # @option options [Array<String>] :items The names of the items that this request pertains to. Required if
22
- # `:operation` is `:add` or `:add_silent`.
33
+ # `operation` is `:add` or `:add_silent`.
23
34
  # @option options [Array<String>] :fields The names of the fields that this request pertains to. Required if
24
- # `:operation` is `:add` or `:add_silent`.
25
- # @option options [:raw, :merge, :distinct, :command] :mode The subscription mode.
26
- def execute(options)
27
- result = execute_post_request build_payload(options)
35
+ # `operation` is `:add` or `:add_silent`.
36
+ # @option options [:distinct, :merge] :mode The subscription mode. Required if `operation` is `:add` or
37
+ # `:add_silent`.
38
+ # @option options [String] :selector The selector for table items. Optional.
39
+ def subscription_execute(operation, table, options = {})
40
+ options[:table] = table
28
41
 
29
- raise ProtocolError.new(result[2], result[1]) if result.first == 'ERROR'
42
+ validate_subscription_options operation, options
43
+
44
+ execute operation, options
30
45
  end
31
46
 
32
47
  private
33
48
 
49
+ # Validates the passed subscription control request options.
50
+ def validate_subscription_options(operation, options)
51
+ raise ArgumentError, 'Invalid table' unless options[:table].is_a? Fixnum
52
+ raise ArgumentError, 'Unsupported operation' unless [:add, :add_silent, :start, :delete].include? operation
53
+
54
+ validate_add_subscription_options options if [:add, :add_silent].include? operation
55
+ end
56
+
57
+ # Validates options required for subscription control requests that perform `add` operations.
58
+ def validate_add_subscription_options(options)
59
+ raise ArgumentError, 'Items not specified' if Array(options[:items]).empty?
60
+ raise ArgumentError, 'Fields not specified' if Array(options[:fields]).empty?
61
+ raise ArgumentError, 'Unsupported mode' unless [:distinct, :merge].include? options[:mode]
62
+ end
63
+
34
64
  # Executes a POST request to the control address with the specified payload. Raises {RequestError} if the HTTP
35
65
  # request fails. Returns the response body split into individual lines.
36
66
  def execute_post_request(payload)
@@ -43,23 +73,39 @@ module Lightstreamer
43
73
 
44
74
  # Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
45
75
  # details on the supported keys.
46
- def build_payload(options)
47
- params = {
48
- LS_session: @session_id,
49
- LS_table: options.fetch(:table),
50
- LS_op: options.fetch(:operation)
51
- }
76
+ def build_payload(operation, options)
77
+ params = {}
52
78
 
53
79
  build_optional_payload_fields options, params
54
80
 
81
+ params[:LS_session] = @session_id
82
+ params[:LS_op] = operation
83
+
55
84
  params
56
85
  end
57
86
 
87
+ OPTION_NAME_TO_API_PARAMETER = {
88
+ table: :LS_table,
89
+ adapter: :LS_data_adapter,
90
+ items: :LS_id,
91
+ fields: :LS_schema,
92
+ selector: :LS_selector,
93
+ maximum_update_frequency: :LS_requested_max_frequency
94
+ }.freeze
95
+
58
96
  def build_optional_payload_fields(options, params)
59
- params[:LS_data_adapter] = options[:adapter] if options.key? :adapter
60
- params[:LS_id] = options[:items].join(' ') if options.key? :items
61
- params[:LS_schema] = options[:fields].map(&:to_s).join(' ') if options.key? :fields
62
- params[:LS_mode] = options[:mode].to_s.upcase if options.key? :mode
97
+ params[:LS_mode] = options[:mode].to_s.upcase if options[:mode]
98
+
99
+ options.each do |key, value|
100
+ next if key == :mode
101
+ next if value.nil?
102
+
103
+ value = value.map(&:to_s).join(' ') if value.is_a? Array
104
+
105
+ api_parameter = OPTION_NAME_TO_API_PARAMETER.fetch key
106
+
107
+ params[api_parameter] = value
108
+ end
63
109
  end
64
110
  end
65
111
  end
@@ -0,0 +1,206 @@
1
+ module Lightstreamer
2
+ # Base class for all errors raised by this gem.
3
+ class Error < StandardError
4
+ end
5
+
6
+ # This error is raised when the session username and password check fails.
7
+ class AuthenticationError < Error
8
+ end
9
+
10
+ # This error is raised when the requested adapter set is unknown.
11
+ class UnknownAdapterSetError < Error
12
+ end
13
+
14
+ # This error is raise when trying to bind to a session that was initialized with a different and incompatible
15
+ # communication protocol.
16
+ class IncompatibleSessionError < Error
17
+ end
18
+
19
+ # This error is raised when the licensed maximum number of sessions is reached.
20
+ class LicensedMaximumSessionsReachedError < Error
21
+ end
22
+
23
+ # This error is raised when the configured maximum number of sessions is reached.
24
+ class ConfiguredMaximumSessionsReachedError < Error
25
+ end
26
+
27
+ # This error is raised when the configured maximum server load is reached.
28
+ class ConfiguredMaximumServerLoadReachedError < Error
29
+ end
30
+
31
+ # This error is raised when the creation of new sessions has been temporarily blocked.
32
+ class NewSessionsTemporarilyBlockedError < Error
33
+ end
34
+
35
+ # This error is raised when streaming is not available because of the current license terms.
36
+ class StreamingNotAvailableError < Error
37
+ end
38
+
39
+ # This error is raised when the specified table can't be modified because it is configured for unfiltered dispatching.
40
+ class TableModificationNotAllowedError < Error
41
+ end
42
+
43
+ # This error is raised when the specified data adapter is invalid or the data adapter is not specified and there is
44
+ # no default data adapter.
45
+ class InvalidDataAdapterError < Error
46
+ end
47
+
48
+ # This error occurs when the specified table is not found.
49
+ class UnknownTableError < Error
50
+ end
51
+
52
+ # This error is raised when an invalid item name is specified.
53
+ class InvalidItemError < Error
54
+ end
55
+
56
+ # This error is raised when an invalid item name for the given fields is specified.
57
+ class InvalidItemForFieldsError < Error
58
+ end
59
+
60
+ # This error is raised when an invalid field name is specified.
61
+ class InvalidFieldError < Error
62
+ end
63
+
64
+ # This error is raised when the specified subscription mode is not supported by one of the items.
65
+ class UnsupportedModeForItemError < Error
66
+ end
67
+
68
+ # This error is raised when an invalid selector is specified.
69
+ class InvalidSelectorError < Error
70
+ end
71
+
72
+ # This error is raised when unfiltered dispatching is requested on an item that does not allow it.
73
+ class UnfilteredDispatchingNotAllowedForItemError < Error
74
+ end
75
+
76
+ # This error is raised when unfiltered dispatching is requested on an item that does not support it.
77
+ class UnfilteredDispatchingNotSupportedForItemError < Error
78
+ end
79
+
80
+ # This error is raised when unfiltered dispatching is requested but is not allowed by the current license terms.
81
+ class UnfilteredDispatchingNotAllowedByLicenseError < Error
82
+ end
83
+
84
+ # This error is raised when `RAW` mode was requested but is not allowed by the current license terms.
85
+ class RawModeNotAllowedByLicenseError < Error
86
+ end
87
+
88
+ # This error is raised when subscriptions are not allowed by the current license terms.
89
+ class SubscriptionsNotAllowedByLicenseError < Error
90
+ end
91
+
92
+ # This error is raised when the specified progressive sequence number for the custom message was invalid.
93
+ class InvalidProgressiveNumberError < Error
94
+ end
95
+
96
+ # This error is raised when the client version requested is not supported by the server.
97
+ class ClientVersionNotSupportedError < Error
98
+ end
99
+
100
+ # This error is raised when a error defined by a metadata adapter is raised.
101
+ class MetadataAdapterError < Error
102
+ # @return [String] The error message from the metadata adapter.
103
+ attr_reader :adapter_error_message
104
+
105
+ # @return [Fixnum] The error code from the metadata adapter.
106
+ attr_reader :adapter_error_code
107
+
108
+ # Initializes this metadata adapter error with the specified error message and error code.
109
+ def initialize(message, code)
110
+ @adapter_error_message = message
111
+ @adapter_error_code = code.to_i
112
+
113
+ super message
114
+ end
115
+ end
116
+
117
+ # This error is raised when a sync error occurs, which most often means that the session ID provided is invalid and
118
+ # a new session needs to be created.
119
+ class SyncError < Error
120
+ end
121
+
122
+ # This error is raised when the specified session ID is for a session that has been terminated.
123
+ class SessionEndError < Error
124
+ # @return [Fixnum] The cause code specifying why the session was terminated by the server, or `nil` if unknown.
125
+ attr_reader :cause_code
126
+
127
+ # Initializes this session end error with the specified cause code.
128
+ #
129
+ # @param [Fixnum] cause_code
130
+ def initialize(cause_code = nil)
131
+ @cause_code = cause_code.to_i
132
+ super()
133
+ end
134
+ end
135
+
136
+ # This error is raised when an HTTP request error occurs.
137
+ class RequestError < Error
138
+ # @return [String] A description of the request error that occurred.
139
+ attr_reader :request_error_message
140
+
141
+ # @return [Fixnum] The HTTP code that was returned, or zero if unknown.
142
+ attr_reader :request_error_code
143
+
144
+ # Initializes this request error with a message and an HTTP code.
145
+ #
146
+ # @param [String] message The error description.
147
+ # @param [Integer] code The HTTP code for the request failure, if known.
148
+ def initialize(message, code)
149
+ @request_error_message = message
150
+ @request_error_code = code.to_i
151
+
152
+ super "Request error #{code}: #{message}"
153
+ end
154
+ end
155
+
156
+ # Base class for all errors raised by this gem.
157
+ class Error
158
+ # Takes a Lightstreamer error message and numeric code and returns an instance of the relevant error class that
159
+ # should be raised in response to the error.
160
+ #
161
+ # @param [String] message The error message.
162
+ # @param [Fixnum] code The numeric error code that is used to determine which {Error} subclass to instantiate.
163
+ #
164
+ # @return [Error]
165
+ def self.build(message, code)
166
+ code = code.to_i
167
+
168
+ if API_ERROR_CODE_TO_CLASS.key? code
169
+ API_ERROR_CODE_TO_CLASS[code].new
170
+ elsif code <= 0
171
+ MetadataAdapterError.new message, code
172
+ else
173
+ new message
174
+ end
175
+ end
176
+
177
+ API_ERROR_CODE_TO_CLASS = {
178
+ 1 => AuthenticationError,
179
+ 2 => UnknownAdapterSetError,
180
+ 3 => IncompatibleSessionError,
181
+ 7 => LicensedMaximumSessionsReachedError,
182
+ 8 => ConfiguredMaximumSessionsReachedError,
183
+ 9 => ConfiguredMaximumServerLoadReachedError,
184
+ 10 => NewSessionsTemporarilyBlockedError,
185
+ 11 => StreamingNotAvailableError,
186
+ 13 => TableModificationNotAllowedError,
187
+ 17 => InvalidDataAdapterError,
188
+ 19 => UnknownTableError,
189
+ 21 => InvalidItemError,
190
+ 22 => InvalidItemForFieldsError,
191
+ 23 => InvalidFieldError,
192
+ 24 => UnsupportedModeForItemError,
193
+ 25 => InvalidSelectorError,
194
+ 26 => UnfilteredDispatchingNotAllowedForItemError,
195
+ 27 => UnfilteredDispatchingNotSupportedForItemError,
196
+ 28 => UnfilteredDispatchingNotAllowedByLicenseError,
197
+ 29 => RawModeNotAllowedByLicenseError,
198
+ 30 => SubscriptionsNotAllowedByLicenseError,
199
+ 32 => InvalidProgressiveNumberError,
200
+ 33 => InvalidProgressiveNumberError,
201
+ 60 => ClientVersionNotSupportedError
202
+ }.freeze
203
+
204
+ private_constant :API_ERROR_CODE_TO_CLASS
205
+ end
206
+ end
@@ -14,6 +14,11 @@ module Lightstreamer
14
14
  # @return [String] The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
15
15
  attr_reader :adapter_set
16
16
 
17
+ # @return [Error] If an error occurs on the stream connection that causes the session to terminate then details of
18
+ # the error will be stored in this attribute. If the session is terminated as a result of calling
19
+ # {#disconnect} then the error will be {SessionEndError}.
20
+ attr_reader :error
21
+
17
22
  # Initializes this new Lightstreamer session with the passed options.
18
23
  #
19
24
  # @param [Hash] options The options to create the session with.
@@ -32,10 +37,12 @@ module Lightstreamer
32
37
  end
33
38
 
34
39
  # Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
35
- # {ProtocolError} will be raised.
40
+ # an {Error} subclass will be raised.
36
41
  def connect
37
42
  return if @stream_connection
38
43
 
44
+ @error = nil
45
+
39
46
  create_stream_connection
40
47
  create_control_connection
41
48
  create_processing_thread
@@ -51,20 +58,31 @@ module Lightstreamer
51
58
  !@stream_connection.nil?
52
59
  end
53
60
 
54
- # Disconnects this session and shuts down its stream connection and processing threads.
61
+ # Disconnects this session and terminates the session on the server. All worker threads are exited.
55
62
  def disconnect
56
- @stream_connection.disconnect if @stream_connection
63
+ @control_connection.execute :destroy if @control_connection
57
64
 
58
- if @processing_thread
59
- Thread.kill @processing_thread
60
- @processing_thread.join
61
- end
65
+ @processing_thread.join 5 if @processing_thread
66
+ ensure
67
+ @stream_connection.disconnect if @stream_connection
68
+ @processing_thread.exit if @processing_thread
62
69
 
63
- @processing_thread = @stream_connection = @control_connection = nil
70
+ @processing_thread = @control_connection = @stream_connection = nil
64
71
  @subscriptions = []
65
72
  end
66
73
 
67
- # Subscribes this Lightstreamer session to the specified subscription.
74
+ # Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
75
+ # stream connection be initiated by the client. The Lightstreamer server requires closure and re-establishment of
76
+ # the stream connection periodically during normal operation, this method just allows such a reconnection to be
77
+ # requested explicitly by the client. If an error occurs then an {Error} subclass will be raised.
78
+ def force_rebind
79
+ return unless @stream_connection
80
+
81
+ @control_connection.execute :force_rebind
82
+ end
83
+
84
+ # Subscribes this Lightstreamer session to the specified subscription. If an error occurs then an {Error} subclass
85
+ # will be raised.
68
86
  #
69
87
  # @param [Subscription] subscription The new subscription to subscribe to.
70
88
  def subscribe(subscription)
@@ -72,14 +90,14 @@ module Lightstreamer
72
90
 
73
91
  @subscriptions_mutex.synchronize { @subscriptions << subscription }
74
92
 
75
- begin
76
- @control_connection.execute table: subscription.id, operation: :add, mode: subscription.mode,
77
- items: subscription.items, fields: subscription.fields,
78
- adapter: subscription.adapter
79
- rescue
80
- @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
81
- raise
82
- end
93
+ options = { mode: subscription.mode, items: subscription.items, fields: subscription.fields,
94
+ adapter: subscription.adapter, maximum_update_frequency: subscription.maximum_update_frequency,
95
+ selector: subscription.selector }
96
+
97
+ @control_connection.subscription_execute :add, subscription.id, options
98
+ rescue
99
+ @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
100
+ raise
83
101
  end
84
102
 
85
103
  # Returns whether the specified subscription is currently active on this session.
@@ -97,7 +115,7 @@ module Lightstreamer
97
115
  def unsubscribe(subscription)
98
116
  raise ArgumentError, 'Unknown subscription' unless subscribed? subscription
99
117
 
100
- @control_connection.execute table: subscription.id, operation: :delete
118
+ @control_connection.subscription_execute :delete, subscription.id
101
119
 
102
120
  @subscriptions_mutex.synchronize { @subscriptions.delete subscription }
103
121
  end
@@ -123,6 +141,8 @@ module Lightstreamer
123
141
  # Starts the processing thread that reads and processes incoming data from the stream connection.
124
142
  def create_processing_thread
125
143
  @processing_thread = Thread.new do
144
+ Thread.current.abort_on_exception = true
145
+
126
146
  loop do
127
147
  line = @stream_connection.read_line
128
148
 
@@ -131,9 +151,9 @@ module Lightstreamer
131
151
  process_stream_data line unless line.empty?
132
152
  end
133
153
 
134
- # The stream connection has died, so exit the processing thread
135
- warn "Lightstreamer: processing thread exiting, error: #{@stream_connection.error}"
136
- @processing_thread = @stream_connection = @control_connection = nil
154
+ # The stream connection has terminated so the session is assumed to be over
155
+ @error = @stream_connection.error
156
+ @processing_thread = @control_connection = @stream_connection = nil
137
157
  end
138
158
  end
139
159
 
@@ -11,8 +11,8 @@ module Lightstreamer
11
11
  # @return [String] The control address returned from the server when this stream connection was initiated.
12
12
  attr_reader :control_address
13
13
 
14
- # @return [ProtocolError, RequestError, String] If an error occurs on the stream thread that causes this stream
15
- # to disconnect then the exception or error details will be stored in this attribute.
14
+ # @return [Error] If an error occurs on the stream thread that causes the stream to disconnect then the
15
+ # error will be stored in this attribute.
16
16
  attr_reader :error
17
17
 
18
18
  # Establishes a new stream connection using the authentication details from the passed session.
@@ -27,10 +27,11 @@ module Lightstreamer
27
27
  end
28
28
 
29
29
  # Establishes a new stream connection using the authentication details from the session that was passed to
30
- # {#initialize}. Raises {ProtocolError} or {RequestError} on failure.
30
+ # {#initialize}. Raises an {Error} subclass on failure.
31
31
  def connect
32
32
  return if @thread
33
33
  @session_id = @error = nil
34
+ @queue.clear
34
35
 
35
36
  create_stream_thread
36
37
 
@@ -51,7 +52,7 @@ module Lightstreamer
51
52
  # Disconnects this stream connection by shutting down the streaming thread.
52
53
  def disconnect
53
54
  if @thread
54
- Thread.kill @thread
55
+ @thread.exit
55
56
  @thread.join
56
57
  end
57
58
 
@@ -73,22 +74,17 @@ module Lightstreamer
73
74
 
74
75
  def create_stream_thread
75
76
  @thread = Thread.new do
76
- begin
77
- stream_thread_main
78
- rescue StandardError => error
79
- @error = error
80
- end
77
+ Thread.current.abort_on_exception = true
81
78
 
82
- @thread = nil
83
- end
84
- end
79
+ connect_stream_and_process_data stream_create_post_request
85
80
 
86
- def stream_thread_main
87
- connect_stream_and_process_data stream_create_post_request
81
+ while @loop
82
+ @loop = false
83
+ connect_stream_and_process_data stream_bind_post_request
84
+ end
88
85
 
89
- while @loop
90
- @loop = false
91
- connect_stream_and_process_data stream_bind_post_request
86
+ @thread = nil
87
+ @queue.push nil
92
88
  end
93
89
  end
94
90
 
@@ -114,6 +110,7 @@ module Lightstreamer
114
110
  end
115
111
 
116
112
  request.on_complete do |response|
113
+ @error = @header.error if @header
117
114
  @error = RequestError.new(response.return_message, response.response_code) unless response.success?
118
115
  end
119
116
 
@@ -133,7 +130,6 @@ module Lightstreamer
133
130
 
134
131
  @control_address = @header['ControlAddress']
135
132
  @session_id = @header['SessionId']
136
- @error = @header.error
137
133
 
138
134
  @header = nil
139
135
  end
@@ -141,6 +137,8 @@ module Lightstreamer
141
137
  def process_body_line(line)
142
138
  if line == 'LOOP'
143
139
  @loop = true
140
+ elsif line =~ /^END/
141
+ @error = SessionEndError.new line[4..-1]
144
142
  elsif !ignore_line?(line)
145
143
  @queue.push line
146
144
  end
@@ -2,8 +2,8 @@ module Lightstreamer
2
2
  # Helper class that processes the contents of the header returned by the server when a new stream connection is
3
3
  # created or an existing session is bound to.
4
4
  class StreamConnectionHeader
5
- # @return [ProtocolError, RequestError] If there was an error in the header then this value will be set to the
6
- # error instance that should be raised in response.
5
+ # @return [Error] If there was an error in the header then this value will be set to the error instance that should
6
+ # be raised in response.
7
7
  attr_reader :error
8
8
 
9
9
  def initialize
@@ -20,21 +20,16 @@ module Lightstreamer
20
20
  def process_header_line(line)
21
21
  @lines << line
22
22
 
23
- unless %w(OK ERROR).include? @lines.first
24
- @error = RequestError.new line
25
- return false
26
- end
23
+ return process_success if @lines.first == 'OK'
24
+ return process_error if @lines.first == 'ERROR'
25
+ return process_end if @lines.first == 'END'
26
+ return process_sync_error if @lines.first == 'SYNC ERROR'
27
27
 
28
- return true if @lines.first == 'OK' && !line.empty?
29
- return true if @lines.first == 'ERROR' && @lines.size < 3
30
-
31
- parse_header
32
-
33
- false
28
+ process_unrecognized
34
29
  end
35
30
 
36
31
  # Returns the value for the item with the specified name in this header, or `nil` if no item with the specified name
37
- # was part of this header
32
+ # was part of this header.
38
33
  #
39
34
  # @param [String] item_name The name of the item to return the header value for.
40
35
  #
@@ -46,15 +41,31 @@ module Lightstreamer
46
41
 
47
42
  private
48
43
 
49
- def parse_header
50
- if @lines.first == 'OK'
51
- @lines[1..-1].each do |line|
52
- match = line.match(/^([^:]*):(.*)$/)
53
- @data[match.captures[0]] = match.captures[1] if match
54
- end
55
- elsif @lines.first == 'ERROR'
56
- @error = ProtocolError.new @lines[2], @lines[1]
57
- end
44
+ def process_success
45
+ match = @lines.last.match(/^([^:]*):(.*)$/)
46
+ @data[match.captures[0]] = match.captures[1] if match
47
+
48
+ !@lines.last.empty?
49
+ end
50
+
51
+ def process_error
52
+ @error = Error.build @lines[2], @lines[1]
53
+ true
54
+ end
55
+
56
+ def process_end
57
+ @error = SessionEndError.new @lines[1]
58
+ true
59
+ end
60
+
61
+ def process_sync_error
62
+ @error = SyncError.new
63
+ false
64
+ end
65
+
66
+ def process_unrecognized
67
+ @error = Error.new @lines.join(' ')
68
+ true
58
69
  end
59
70
  end
60
71
  end
@@ -1,8 +1,8 @@
1
1
  module Lightstreamer
2
- # Describes a subscription that can be bound to a Lightstreamer session in order to consume its data. A subscription
3
- # is described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous
4
- # data callback using {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data
5
- # only once they are attached to a Lightstreamer session using {Session#subscribe}.
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 {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data only
5
+ # once they are attached to a session using {Session#subscribe}.
6
6
  class Subscription
7
7
  # @return [Fixnum] The unique identification number of this subscription. This is used to identify the subscription
8
8
  # in incoming Lightstreamer data.
@@ -21,6 +21,13 @@ module Lightstreamer
21
21
  # If `nil` then the default data adapter will be used.
22
22
  attr_reader :adapter
23
23
 
24
+ # @return [String] The selector for table items. Optional.
25
+ attr_reader :selector
26
+
27
+ # @return [Float] The maximum number of updates this subscription should receive per second. If this is set to zero,
28
+ # which is the default, then there is no limit on the update frequency.
29
+ attr_reader :maximum_update_frequency
30
+
24
31
  # Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
25
32
  # {Session#subscribe} to activate the subscription on a Lightstreamer session.
26
33
  #
@@ -30,13 +37,18 @@ module Lightstreamer
30
37
  # @option options [:distinct, :merge] :mode The operation mode of this subscription.
31
38
  # @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
32
39
  # should be used. If `nil` then the default data adapter will be used.
40
+ # @option options [String] :selector The selector for table items. Optional.
41
+ # @option options [Float] :maximum_update_frequency The maximum number of updates this subscription should receive
42
+ # per second. Defaults to zero which means there is no limit on the update frequency.
33
43
  def initialize(options)
34
44
  @id = self.class.next_id
35
45
 
36
- @items = Array(options.fetch(:items))
37
- @fields = Array(options.fetch(:fields))
46
+ @items = options.fetch(:items)
47
+ @fields = options.fetch(:fields)
38
48
  @mode = options.fetch(:mode).to_sym
39
49
  @adapter = options[:adapter]
50
+ @selector = options[:selector]
51
+ @maximum_update_frequency = options[:maximum_update_frequency] || 0.0
40
52
 
41
53
  @data_mutex = Mutex.new
42
54
  clear_data
@@ -3,20 +3,29 @@ module Lightstreamer
3
3
  module UTF16
4
4
  module_function
5
5
 
6
- # Decodes any UTF-16 escape sequences in the form '\uXXXX' into a new string. Invalid escape sequences are removed.
6
+ # Decodes any UTF-16 escape sequences in the form '\uXXXX' in the passed string. Invalid escape sequences are
7
+ # removed.
8
+ #
9
+ # @param [String] string The string to decode.
10
+ #
11
+ # @return [String]
7
12
  def decode_escape_sequences(string)
8
13
  string = decode_surrogate_pairs_escape_sequences string
9
14
 
10
- # Match all remaining escape sequences
15
+ # Match all escape sequences
11
16
  string.gsub(/\\u[A-F\d]{4}/i) do |escape_sequence|
12
17
  codepoint = escape_sequence[2..-1].hex
13
18
 
14
- # Codepoints greater than 0xD7FF are invalid
19
+ # Codepoints greater than 0xD7FF are invalid are ignored
15
20
  codepoint < 0xD800 ? [codepoint].pack('U') : ''
16
21
  end
17
22
  end
18
23
 
19
- # Converts any UTF-16 surrogate pairs escape sequences in the form '\uXXXX\uYYYY' into UTF-8.
24
+ # Decodes any UTF-16 surrogate pairs escape sequences in the form '\uXXXX\uYYYY' in the passed string.
25
+ #
26
+ # @param [String] string The string to decode.
27
+ #
28
+ # @return [String]
20
29
  def decode_surrogate_pairs_escape_sequences(string)
21
30
  string.gsub(/\\uD[89AB][A-F\d]{2}\\uD[C-F][A-F\d]{2}/i) do |escape_sequence|
22
31
  high_surrogate = escape_sequence[2...6].hex
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.3'.freeze
3
+ VERSION = '0.4'.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.3'
4
+ version: '0.4'
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-24 00:00:00.000000000 Z
11
+ date: 2016-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -179,9 +179,8 @@ files:
179
179
  - lib/lightstreamer/cli/commands/stream_command.rb
180
180
  - lib/lightstreamer/cli/main.rb
181
181
  - lib/lightstreamer/control_connection.rb
182
+ - lib/lightstreamer/errors.rb
182
183
  - lib/lightstreamer/line_buffer.rb
183
- - lib/lightstreamer/protocol_error.rb
184
- - lib/lightstreamer/request_error.rb
185
184
  - lib/lightstreamer/session.rb
186
185
  - lib/lightstreamer/stream_connection.rb
187
186
  - lib/lightstreamer/stream_connection_header.rb
@@ -1,22 +0,0 @@
1
- module Lightstreamer
2
- # This error class is raised by {Session} when a request to the Lightstreamer API fails with a Lightstreamer-specific
3
- # error code and error message.
4
- class ProtocolError < StandardError
5
- # @return [String] A description of the Lightstreamer error that occurred.
6
- attr_reader :error
7
-
8
- # @return [Fixnum] The numeric code of the Lightstreamer error.
9
- attr_reader :code
10
-
11
- # Initializes this protocol error with the specific message and code.
12
- #
13
- # @param [String] error The error description.
14
- # @param [Integer] code The numeric code for the error.
15
- def initialize(error, code)
16
- @error = error.to_s
17
- @code = code.to_i
18
-
19
- super "Lightstreamer error: #{@error}, code: #{code}"
20
- end
21
- end
22
- end
@@ -1,21 +0,0 @@
1
- module Lightstreamer
2
- # This error class is raised by {Session} when a request to the Lightstreamer API fails.
3
- class RequestError < StandardError
4
- # @return [String] A description of the error that occurred when the request was attempted.
5
- attr_reader :error
6
-
7
- # @return [Fixnum] The HTTP code that was returned, or `nil` if unknown.
8
- attr_reader :http_code
9
-
10
- # Initializes this request error with a message and an HTTP code.
11
- #
12
- # @param [String] error The error description.
13
- # @param [Integer] http_code The HTTP code for the request failure, if known.
14
- def initialize(error, http_code = nil)
15
- @error = error.to_s
16
- @http_code = http_code ? http_code.to_i : nil
17
-
18
- super "Request error: #{error}#{http_code ? ", http code: #{http_code}" : ''}"
19
- end
20
- end
21
- end