lightstreamer 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/lightstreamer.rb +1 -2
- data/lib/lightstreamer/cli/commands/stream_command.rb +10 -2
- data/lib/lightstreamer/control_connection.rb +67 -21
- data/lib/lightstreamer/errors.rb +206 -0
- data/lib/lightstreamer/session.rb +41 -21
- data/lib/lightstreamer/stream_connection.rb +16 -18
- data/lib/lightstreamer/stream_connection_header.rb +33 -22
- data/lib/lightstreamer/subscription.rb +18 -6
- data/lib/lightstreamer/utf16.rb +13 -4
- data/lib/lightstreamer/version.rb +1 -1
- metadata +3 -4
- data/lib/lightstreamer/protocol_error.rb +0 -22
- data/lib/lightstreamer/request_error.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 863ad0ba9936b00dca802004782358a03cf5cd36
|
4
|
+
data.tar.gz: a13adc3e4b6b9945580bc02927826d61742dba1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
15
|
-
# {
|
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 [
|
18
|
-
# @
|
19
|
-
|
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
|
-
#
|
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
|
-
#
|
25
|
-
# @option options [:
|
26
|
-
|
27
|
-
|
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
|
-
|
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[:
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
# {
|
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
|
61
|
+
# Disconnects this session and terminates the session on the server. All worker threads are exited.
|
55
62
|
def disconnect
|
56
|
-
@
|
63
|
+
@control_connection.execute :destroy if @control_connection
|
57
64
|
|
58
|
-
if @processing_thread
|
59
|
-
|
60
|
-
|
61
|
-
|
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 = @
|
70
|
+
@processing_thread = @control_connection = @stream_connection = nil
|
64
71
|
@subscriptions = []
|
65
72
|
end
|
66
73
|
|
67
|
-
#
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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.
|
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
|
135
|
-
|
136
|
-
@processing_thread = @
|
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 [
|
15
|
-
#
|
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 {
|
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
|
-
|
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
|
-
|
77
|
-
stream_thread_main
|
78
|
-
rescue StandardError => error
|
79
|
-
@error = error
|
80
|
-
end
|
77
|
+
Thread.current.abort_on_exception = true
|
81
78
|
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|
79
|
+
connect_stream_and_process_data stream_create_post_request
|
85
80
|
|
86
|
-
|
87
|
-
|
81
|
+
while @loop
|
82
|
+
@loop = false
|
83
|
+
connect_stream_and_process_data stream_bind_post_request
|
84
|
+
end
|
88
85
|
|
89
|
-
|
90
|
-
@
|
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 [
|
6
|
-
#
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
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 =
|
37
|
-
@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
|
data/lib/lightstreamer/utf16.rb
CHANGED
@@ -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'
|
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
|
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
|
-
#
|
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
|
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.
|
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-
|
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
|