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 +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
|