lightstreamer 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/lib/lightstreamer.rb +4 -4
- data/lib/lightstreamer/cli/commands/stream_command.rb +11 -4
- data/lib/lightstreamer/control_connection.rb +7 -13
- data/lib/lightstreamer/errors.rb +76 -45
- data/lib/lightstreamer/line_buffer.rb +7 -2
- data/lib/lightstreamer/messages/overflow_message.rb +40 -0
- data/lib/lightstreamer/messages/update_message.rb +86 -0
- data/lib/lightstreamer/session.rb +24 -14
- data/lib/lightstreamer/stream_connection.rb +50 -35
- data/lib/lightstreamer/stream_connection_header.rb +10 -6
- data/lib/lightstreamer/subscription.rb +88 -104
- data/lib/lightstreamer/version.rb +1 -1
- metadata +4 -3
- data/lib/lightstreamer/utf16.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c64dd0d680841a38eb530d0a17fa0700985426a
|
4
|
+
data.tar.gz: 5a66944aff7e81aeef70c8cb5551703c1389c281
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dca8fbcb4375c44451946467847f764b072e86b45d7f7c6d50b4240e9c8a4ae3d3eac5cbb3ee2019365e2436bdcadbeb21bda2dd9eb2a9bdd89bed5bd6657bcd
|
7
|
+
data.tar.gz: 1cd6cc4234ddcf107194d59d4443a69e3ffa4f6ce2bfed5671f5e5ac72c33bc9a64ccfda4086fca1901489133e7f43f32ef787bf32fc1b3a57304dd2fec43c78
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Lightstreamer Changelog
|
2
2
|
|
3
|
+
### 0.5 — July 26, 2016
|
4
|
+
|
5
|
+
- Improved handling of `:distinct` subscriptions
|
6
|
+
- Subscriptions can now request an unfiltered stream and handle any overflow messages from the server using
|
7
|
+
`Lightstreamer::Subscription#on_callback`
|
8
|
+
- Added a connection timeout to all requests
|
9
|
+
- Unhandled exceptions in subscription data callbacks are no longer automatically rescued
|
10
|
+
- Improved API documentation
|
11
|
+
- Renamed `Lightstreamer::Subscription#add_data_callback` to `Lightstreamer::Subscription#on_data`
|
12
|
+
- Replaced `Lightstreamer::Subscription#remove_data_callback` with `Lightstreamer::Subscription#clear_callbacks`
|
13
|
+
- Renamed `Lightstreamer::Error` class to `Lightstreamer::LightstreamerError`
|
14
|
+
- Renamed `Lightstreamer::Subscription#retrieve_item_data` to `Lightstreamer::Subscription#item_data`
|
15
|
+
- Removed `Lightstreamer::Subscription#clear_data_for_item`
|
16
|
+
|
3
17
|
### 0.4 — July 25, 2016
|
4
18
|
|
5
19
|
- Added support for specifying a subscription's selector and maximum update frequency
|
data/README.md
CHANGED
@@ -53,7 +53,7 @@ queue = Queue.new
|
|
53
53
|
|
54
54
|
# When new data becomes available for the subscription it will be put on the queue. This callback
|
55
55
|
# will be run on a worker thread.
|
56
|
-
subscription.
|
56
|
+
subscription.on_data do |subscription, item_name, item_data, new_values|
|
57
57
|
queue.push item_data
|
58
58
|
end
|
59
59
|
|
data/lib/lightstreamer.rb
CHANGED
@@ -2,19 +2,19 @@ require 'thor'
|
|
2
2
|
require 'typhoeus'
|
3
3
|
require 'uri'
|
4
4
|
|
5
|
+
require 'lightstreamer/cli/main'
|
6
|
+
require 'lightstreamer/cli/commands/stream_command'
|
5
7
|
require 'lightstreamer/control_connection'
|
6
8
|
require 'lightstreamer/errors'
|
7
9
|
require 'lightstreamer/line_buffer'
|
10
|
+
require 'lightstreamer/messages/overflow_message'
|
11
|
+
require 'lightstreamer/messages/update_message'
|
8
12
|
require 'lightstreamer/session'
|
9
13
|
require 'lightstreamer/stream_connection'
|
10
14
|
require 'lightstreamer/stream_connection_header'
|
11
15
|
require 'lightstreamer/subscription'
|
12
|
-
require 'lightstreamer/utf16'
|
13
16
|
require 'lightstreamer/version'
|
14
17
|
|
15
|
-
require 'lightstreamer/cli/main'
|
16
|
-
require 'lightstreamer/cli/commands/stream_command'
|
17
|
-
|
18
18
|
# This module contains all the code for the Lightstreamer gem. See `README.md` to get started with using this gem.
|
19
19
|
module Lightstreamer
|
20
20
|
end
|
@@ -13,7 +13,7 @@ module Lightstreamer
|
|
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
15
|
option :selector, desc: 'The selector for table items'
|
16
|
-
option :maximum_update_frequency,
|
16
|
+
option :maximum_update_frequency, desc: 'The maximum number of updates per second for each item'
|
17
17
|
|
18
18
|
def stream
|
19
19
|
session = create_session
|
@@ -24,7 +24,9 @@ module Lightstreamer
|
|
24
24
|
session.subscribe create_subscription
|
25
25
|
|
26
26
|
loop do
|
27
|
-
puts @queue.pop
|
27
|
+
puts @queue.pop unless @queue.empty?
|
28
|
+
|
29
|
+
raise session.error if session.error
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
@@ -40,7 +42,8 @@ module Lightstreamer
|
|
40
42
|
def create_subscription
|
41
43
|
subscription = Lightstreamer::Subscription.new subscription_options
|
42
44
|
|
43
|
-
subscription.
|
45
|
+
subscription.on_data(&method(:on_data))
|
46
|
+
subscription.on_overflow(&method(:on_overflow))
|
44
47
|
|
45
48
|
subscription
|
46
49
|
end
|
@@ -52,9 +55,13 @@ module Lightstreamer
|
|
52
55
|
}
|
53
56
|
end
|
54
57
|
|
55
|
-
def
|
58
|
+
def on_data(_subscription, item_name, _item_data, new_values)
|
56
59
|
@queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
|
57
60
|
end
|
61
|
+
|
62
|
+
def on_overflow(_subscription, item_name, overflow_size)
|
63
|
+
@queue.push "Overflow of size #{overflow_size} on item #{item_name}"
|
64
|
+
end
|
58
65
|
end
|
59
66
|
end
|
60
67
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
#
|
2
|
+
# Helper class used by {Session} and is responsible for sending Lightstreamer control requests.
|
3
|
+
#
|
4
|
+
# @private
|
3
5
|
class ControlConnection
|
4
6
|
# Initializes this class for sending Lightstreamer control requests using the specified session ID and control
|
5
7
|
# address.
|
@@ -12,30 +14,22 @@ module Lightstreamer
|
|
12
14
|
end
|
13
15
|
|
14
16
|
# Sends a Lightstreamer control request that executes the specified operation with the specified options. If an
|
15
|
-
# error occurs then
|
17
|
+
# error occurs then a {LightstreamerError} subclass will be raised.
|
16
18
|
#
|
17
19
|
# @param [String] operation The operation to execute.
|
18
20
|
# @param [Hash] options The options to include on the request.
|
19
21
|
def execute(operation, options = {})
|
20
22
|
result = execute_post_request build_payload(operation, options)
|
21
23
|
|
22
|
-
raise
|
24
|
+
raise LightstreamerError.build(result[2], result[1]) if result.first != 'OK'
|
23
25
|
end
|
24
26
|
|
25
27
|
# Sends a Lightstreamer subscription control request with the specified operation, table, and options. If an error
|
26
|
-
# occurs then
|
28
|
+
# occurs then a {LightstreamerError} subclass will be raised.
|
27
29
|
#
|
28
30
|
# @param [:add, :add_silent, :start, :delete] operation The operation to execute.
|
29
31
|
# @param [Fixnum] table The ID of the table this request pertains to.
|
30
32
|
# @param [Hash] options The subscription control request options.
|
31
|
-
# @option options [String] :adapter The name of the data adapter to use. Optional.
|
32
|
-
# @option options [Array<String>] :items The names of the items that this request pertains to. Required if
|
33
|
-
# `operation` is `:add` or `:add_silent`.
|
34
|
-
# @option options [Array<String>] :fields The names of the fields that this request pertains to. Required if
|
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
33
|
def subscription_execute(operation, table, options = {})
|
40
34
|
options[:table] = table
|
41
35
|
|
@@ -64,7 +58,7 @@ module Lightstreamer
|
|
64
58
|
# Executes a POST request to the control address with the specified payload. Raises {RequestError} if the HTTP
|
65
59
|
# request fails. Returns the response body split into individual lines.
|
66
60
|
def execute_post_request(payload)
|
67
|
-
response = Typhoeus.post @control_url, body: payload
|
61
|
+
response = Typhoeus.post @control_url, body: payload, timeout: 15
|
68
62
|
|
69
63
|
raise RequestError.new(response.return_message, response.response_code) unless response.success?
|
70
64
|
|
data/lib/lightstreamer/errors.rb
CHANGED
@@ -1,114 +1,120 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
|
3
|
-
class Error < StandardError
|
2
|
+
class LightstreamerError < StandardError
|
4
3
|
end
|
5
4
|
|
6
5
|
# This error is raised when the session username and password check fails.
|
7
|
-
class AuthenticationError <
|
6
|
+
class AuthenticationError < LightstreamerError
|
8
7
|
end
|
9
8
|
|
10
9
|
# This error is raised when the requested adapter set is unknown.
|
11
|
-
class UnknownAdapterSetError <
|
10
|
+
class UnknownAdapterSetError < LightstreamerError
|
12
11
|
end
|
13
12
|
|
14
13
|
# This error is raise when trying to bind to a session that was initialized with a different and incompatible
|
15
14
|
# communication protocol.
|
16
|
-
class IncompatibleSessionError <
|
15
|
+
class IncompatibleSessionError < LightstreamerError
|
17
16
|
end
|
18
17
|
|
19
18
|
# This error is raised when the licensed maximum number of sessions is reached.
|
20
|
-
class LicensedMaximumSessionsReachedError <
|
19
|
+
class LicensedMaximumSessionsReachedError < LightstreamerError
|
21
20
|
end
|
22
21
|
|
23
22
|
# This error is raised when the configured maximum number of sessions is reached.
|
24
|
-
class ConfiguredMaximumSessionsReachedError <
|
23
|
+
class ConfiguredMaximumSessionsReachedError < LightstreamerError
|
25
24
|
end
|
26
25
|
|
27
26
|
# This error is raised when the configured maximum server load is reached.
|
28
|
-
class ConfiguredMaximumServerLoadReachedError <
|
27
|
+
class ConfiguredMaximumServerLoadReachedError < LightstreamerError
|
29
28
|
end
|
30
29
|
|
31
30
|
# This error is raised when the creation of new sessions has been temporarily blocked.
|
32
|
-
class NewSessionsTemporarilyBlockedError <
|
31
|
+
class NewSessionsTemporarilyBlockedError < LightstreamerError
|
33
32
|
end
|
34
33
|
|
35
34
|
# This error is raised when streaming is not available because of the current license terms.
|
36
|
-
class StreamingNotAvailableError <
|
35
|
+
class StreamingNotAvailableError < LightstreamerError
|
37
36
|
end
|
38
37
|
|
39
38
|
# This error is raised when the specified table can't be modified because it is configured for unfiltered dispatching.
|
40
|
-
class TableModificationNotAllowedError <
|
39
|
+
class TableModificationNotAllowedError < LightstreamerError
|
41
40
|
end
|
42
41
|
|
43
42
|
# This error is raised when the specified data adapter is invalid or the data adapter is not specified and there is
|
44
43
|
# no default data adapter.
|
45
|
-
class InvalidDataAdapterError <
|
44
|
+
class InvalidDataAdapterError < LightstreamerError
|
46
45
|
end
|
47
46
|
|
48
47
|
# This error occurs when the specified table is not found.
|
49
|
-
class UnknownTableError <
|
48
|
+
class UnknownTableError < LightstreamerError
|
50
49
|
end
|
51
50
|
|
52
51
|
# This error is raised when an invalid item name is specified.
|
53
|
-
class InvalidItemError <
|
52
|
+
class InvalidItemError < LightstreamerError
|
54
53
|
end
|
55
54
|
|
56
55
|
# This error is raised when an invalid item name for the given fields is specified.
|
57
|
-
class InvalidItemForFieldsError <
|
56
|
+
class InvalidItemForFieldsError < LightstreamerError
|
58
57
|
end
|
59
58
|
|
60
59
|
# This error is raised when an invalid field name is specified.
|
61
|
-
class InvalidFieldError <
|
60
|
+
class InvalidFieldError < LightstreamerError
|
62
61
|
end
|
63
62
|
|
64
63
|
# This error is raised when the specified subscription mode is not supported by one of the items.
|
65
|
-
class UnsupportedModeForItemError <
|
64
|
+
class UnsupportedModeForItemError < LightstreamerError
|
66
65
|
end
|
67
66
|
|
68
67
|
# This error is raised when an invalid selector is specified.
|
69
|
-
class InvalidSelectorError <
|
68
|
+
class InvalidSelectorError < LightstreamerError
|
70
69
|
end
|
71
70
|
|
72
71
|
# This error is raised when unfiltered dispatching is requested on an item that does not allow it.
|
73
|
-
class UnfilteredDispatchingNotAllowedForItemError <
|
72
|
+
class UnfilteredDispatchingNotAllowedForItemError < LightstreamerError
|
74
73
|
end
|
75
74
|
|
76
75
|
# This error is raised when unfiltered dispatching is requested on an item that does not support it.
|
77
|
-
class UnfilteredDispatchingNotSupportedForItemError <
|
76
|
+
class UnfilteredDispatchingNotSupportedForItemError < LightstreamerError
|
78
77
|
end
|
79
78
|
|
80
79
|
# This error is raised when unfiltered dispatching is requested but is not allowed by the current license terms.
|
81
|
-
class UnfilteredDispatchingNotAllowedByLicenseError <
|
80
|
+
class UnfilteredDispatchingNotAllowedByLicenseError < LightstreamerError
|
82
81
|
end
|
83
82
|
|
84
83
|
# This error is raised when `RAW` mode was requested but is not allowed by the current license terms.
|
85
|
-
class RawModeNotAllowedByLicenseError <
|
84
|
+
class RawModeNotAllowedByLicenseError < LightstreamerError
|
86
85
|
end
|
87
86
|
|
88
87
|
# This error is raised when subscriptions are not allowed by the current license terms.
|
89
|
-
class SubscriptionsNotAllowedByLicenseError <
|
88
|
+
class SubscriptionsNotAllowedByLicenseError < LightstreamerError
|
90
89
|
end
|
91
90
|
|
92
91
|
# This error is raised when the specified progressive sequence number for the custom message was invalid.
|
93
|
-
class InvalidProgressiveNumberError <
|
92
|
+
class InvalidProgressiveNumberError < LightstreamerError
|
94
93
|
end
|
95
94
|
|
96
95
|
# This error is raised when the client version requested is not supported by the server.
|
97
|
-
class ClientVersionNotSupportedError <
|
96
|
+
class ClientVersionNotSupportedError < LightstreamerError
|
98
97
|
end
|
99
98
|
|
100
99
|
# This error is raised when a error defined by a metadata adapter is raised.
|
101
|
-
class MetadataAdapterError <
|
102
|
-
#
|
100
|
+
class MetadataAdapterError < LightstreamerError
|
101
|
+
# The error message from the metadata adapter.
|
102
|
+
#
|
103
|
+
# @return [String]
|
103
104
|
attr_reader :adapter_error_message
|
104
105
|
|
105
|
-
#
|
106
|
+
# The error code from the metadata adapter.
|
107
|
+
#
|
108
|
+
# @return [Fixnum]
|
106
109
|
attr_reader :adapter_error_code
|
107
110
|
|
108
111
|
# Initializes this metadata adapter error with the specified error message and error code.
|
112
|
+
#
|
113
|
+
# @param [String] message The error message.
|
114
|
+
# @param [Fixnum] code The error code.
|
109
115
|
def initialize(message, code)
|
110
116
|
@adapter_error_message = message
|
111
|
-
@adapter_error_code = code
|
117
|
+
@adapter_error_code = code
|
112
118
|
|
113
119
|
super message
|
114
120
|
end
|
@@ -116,52 +122,77 @@ module Lightstreamer
|
|
116
122
|
|
117
123
|
# This error is raised when a sync error occurs, which most often means that the session ID provided is invalid and
|
118
124
|
# a new session needs to be created.
|
119
|
-
class SyncError <
|
125
|
+
class SyncError < LightstreamerError
|
120
126
|
end
|
121
127
|
|
122
|
-
# This error is raised when the
|
123
|
-
|
124
|
-
|
128
|
+
# This error is raised when the session was explicitly closed on the server side. The reason for this is specified by
|
129
|
+
# {#cause_code}.
|
130
|
+
class SessionEndError < LightstreamerError
|
131
|
+
# The cause code specifying why the session was terminated by the server, or `nil` if unknown.
|
132
|
+
#
|
133
|
+
# The following codes are defined, but other values are allowed and signal an unexpected cause.
|
134
|
+
#
|
135
|
+
# - `<=0` - The session was closed through a `destroy` request and this custom code was specified.
|
136
|
+
# - `31` - The session was closed through a `destroy` request.
|
137
|
+
# - `32` - The session was closed by an administrator through JMX.
|
138
|
+
# - `33`, `34` - An unexpected error occurred on the server.
|
139
|
+
# - `35` - Another session was opened on the metadata adapter and the metadata adpater only supports one session.
|
140
|
+
# - `40` - A manual rebind to the session was done by another client.
|
141
|
+
# - `48` - The maximum session duration configured on the server has been reached. This is meant as a way to refresh
|
142
|
+
# the session and the client should recover by opening a new session immediately.
|
143
|
+
#
|
144
|
+
# @return [Fixnum, nil]
|
125
145
|
attr_reader :cause_code
|
126
146
|
|
127
147
|
# Initializes this session end error with the specified cause code.
|
128
148
|
#
|
129
|
-
# @param [
|
130
|
-
def initialize(cause_code
|
131
|
-
@cause_code = cause_code.to_i
|
149
|
+
# @param [Session?] cause_code See {#cause_code} for details.
|
150
|
+
def initialize(cause_code)
|
151
|
+
@cause_code = cause_code && cause_code.to_i
|
132
152
|
super()
|
133
153
|
end
|
134
154
|
end
|
135
155
|
|
136
156
|
# This error is raised when an HTTP request error occurs.
|
137
|
-
class RequestError <
|
138
|
-
#
|
157
|
+
class RequestError < LightstreamerError
|
158
|
+
# The description of the request error that occurred.
|
159
|
+
#
|
160
|
+
# @return [String]
|
139
161
|
attr_reader :request_error_message
|
140
162
|
|
141
|
-
#
|
163
|
+
# The HTTP code that was returned, or zero if unknown.
|
164
|
+
#
|
165
|
+
# @return [Fixnum]
|
142
166
|
attr_reader :request_error_code
|
143
167
|
|
144
168
|
# Initializes this request error with a message and an HTTP code.
|
145
169
|
#
|
146
170
|
# @param [String] message The error description.
|
147
|
-
# @param [
|
171
|
+
# @param [Fixnum] code The HTTP code for the request failure, or zero if unknown.
|
148
172
|
def initialize(message, code)
|
149
173
|
@request_error_message = message
|
150
|
-
@request_error_code = code
|
174
|
+
@request_error_code = code
|
151
175
|
|
152
|
-
|
176
|
+
if code != 0
|
177
|
+
super "#{code}: #{message}"
|
178
|
+
else
|
179
|
+
super message
|
180
|
+
end
|
153
181
|
end
|
154
182
|
end
|
155
183
|
|
156
184
|
# Base class for all errors raised by this gem.
|
157
|
-
class
|
185
|
+
class LightstreamerError
|
158
186
|
# Takes a Lightstreamer error message and numeric code and returns an instance of the relevant error class that
|
159
187
|
# should be raised in response to the error.
|
160
188
|
#
|
161
189
|
# @param [String] message The error message.
|
162
|
-
# @param [Fixnum] code The numeric error code that is used to determine which {
|
190
|
+
# @param [Fixnum] code The numeric error code that is used to determine which {LightstreamerError} subclass to
|
191
|
+
# instantiate.
|
163
192
|
#
|
164
193
|
# @return [Error]
|
194
|
+
#
|
195
|
+
# @private
|
165
196
|
def self.build(message, code)
|
166
197
|
code = code.to_i
|
167
198
|
|
@@ -170,7 +201,7 @@ module Lightstreamer
|
|
170
201
|
elsif code <= 0
|
171
202
|
MetadataAdapterError.new message, code
|
172
203
|
else
|
173
|
-
new message
|
204
|
+
new "#{code}: #{message}"
|
174
205
|
end
|
175
206
|
end
|
176
207
|
|
@@ -1,13 +1,18 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
# Helper class that takes an incoming stream of ASCII data and yields back individual lines
|
2
|
+
# Helper class used by {StreamConnection} that takes an incoming stream of ASCII data and yields back individual lines
|
3
|
+
# as they become complete.
|
4
|
+
#
|
5
|
+
# @private
|
3
6
|
class LineBuffer
|
4
7
|
def initialize
|
5
8
|
@buffer = ''
|
6
9
|
end
|
7
10
|
|
8
|
-
# Appends a new piece of ASCII data to this buffer
|
11
|
+
# Appends a new piece of ASCII data to this buffer and yields back any lines that are now complete.
|
9
12
|
#
|
10
13
|
# @param [String] data The new piece of ASCII data.
|
14
|
+
#
|
15
|
+
# @yieldparam [String] line The new line that is now complete.
|
11
16
|
def process(data)
|
12
17
|
@buffer << data
|
13
18
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class used by {Subscription} in order to parse incoming overflow messages.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class OverflowMessage
|
6
|
+
# The index of the item this overflow message applies to.
|
7
|
+
#
|
8
|
+
# @return [Fixnum]
|
9
|
+
attr_accessor :item_index
|
10
|
+
|
11
|
+
# The size of the overflow that occurred.
|
12
|
+
#
|
13
|
+
# @return [Fixnum]
|
14
|
+
attr_accessor :overflow_size
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Attempts to parses the specified line as an overflow message for the given table and items and returns an
|
18
|
+
# instance of {OverflowMessage} on success, or `nil` on failure.
|
19
|
+
def parse(line, table_id, items)
|
20
|
+
message = new
|
21
|
+
|
22
|
+
match = line.match table_regexp(table_id)
|
23
|
+
return unless match
|
24
|
+
|
25
|
+
message.item_index = match.captures[0].to_i - 1
|
26
|
+
return unless message.item_index < items.size
|
27
|
+
|
28
|
+
message.overflow_size = match.captures[1].to_i
|
29
|
+
|
30
|
+
message
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def table_regexp(table_id)
|
36
|
+
Regexp.new "^#{table_id},(\\d+),OV(\\d+)$"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class used by {Subscription} in order to parse incoming update messages.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class UpdateMessage
|
6
|
+
# The index of the item this update message applies to.
|
7
|
+
#
|
8
|
+
# @return [Fixnum]
|
9
|
+
attr_accessor :item_index
|
10
|
+
|
11
|
+
# The field values specified by this update message.
|
12
|
+
#
|
13
|
+
# @return [Array]
|
14
|
+
attr_accessor :values
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Attempts to parses the specified line as an update message for the given table, items, and fields, and returns
|
18
|
+
# an instance of {UpdateMessage} on success, or `nil` on failure.
|
19
|
+
def parse(line, table_id, items, fields)
|
20
|
+
message = new
|
21
|
+
|
22
|
+
match = line.match table_regexp(table_id, fields)
|
23
|
+
return unless match
|
24
|
+
|
25
|
+
message.item_index = match.captures[0].to_i - 1
|
26
|
+
return unless message.item_index < items.size
|
27
|
+
|
28
|
+
message.values = parse_values match.captures[1..-1], fields
|
29
|
+
|
30
|
+
message
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def table_regexp(table_id, fields)
|
36
|
+
Regexp.new "^#{table_id},(\\d+)#{'\|(.*)' * fields.size}$"
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_values(values, fields)
|
40
|
+
hash = {}
|
41
|
+
|
42
|
+
values.each_with_index do |value, index|
|
43
|
+
next if value == ''
|
44
|
+
|
45
|
+
hash[fields[index]] = parse_raw_field_value value
|
46
|
+
end
|
47
|
+
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_raw_field_value(value)
|
52
|
+
return '' if value == '$'
|
53
|
+
return nil if value == '#'
|
54
|
+
|
55
|
+
value = value[1..-1] if value =~ /^(\$|#)/
|
56
|
+
|
57
|
+
decode_escape_sequences value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Decodes any UTF-16 escape sequences in the form '\uXXXX' in the passed string. Invalid escape sequences are
|
61
|
+
# removed.
|
62
|
+
def decode_escape_sequences(string)
|
63
|
+
string = decode_surrogate_pair_escape_sequences string
|
64
|
+
|
65
|
+
string.gsub(/\\u[A-F\d]{4}/i) do |escape_sequence|
|
66
|
+
codepoint = escape_sequence[2..-1].hex
|
67
|
+
|
68
|
+
# Codepoints greater than 0xD7FF are invalid and so are removed
|
69
|
+
codepoint < 0xD800 ? [codepoint].pack('U') : ''
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Decodes any UTF-16 surrogate pair escape sequences in the form '\uXXXX\uYYYY' in the passed string.
|
74
|
+
def decode_surrogate_pair_escape_sequences(string)
|
75
|
+
string.gsub(/\\uD[89AB][A-F\d]{2}\\uD[C-F][A-F\d]{2}/i) do |escape_sequence|
|
76
|
+
high_surrogate = escape_sequence[2...6].hex
|
77
|
+
low_surrogate = escape_sequence[8...12].hex
|
78
|
+
|
79
|
+
codepoint = 0x10000 + ((high_surrogate - 0xD800) << 10) + (low_surrogate - 0xDC00)
|
80
|
+
|
81
|
+
[codepoint].pack 'U'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -2,30 +2,40 @@ module Lightstreamer
|
|
2
2
|
# This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class is the
|
3
3
|
# primary interface for working with Lightstreamer.
|
4
4
|
class Session
|
5
|
-
#
|
5
|
+
# The URL of the Lightstreamer server to connect to. Set by {#initialize}.
|
6
|
+
#
|
7
|
+
# @return [String]
|
6
8
|
attr_reader :server_url
|
7
9
|
|
8
|
-
#
|
10
|
+
# The username to connect to the Lightstreamer server with. Set by {#initialize}.
|
11
|
+
#
|
12
|
+
# @return [String, nil]
|
9
13
|
attr_reader :username
|
10
14
|
|
11
|
-
#
|
15
|
+
# The password to connect to the Lightstreamer server with. Set by {#initialize}.
|
16
|
+
#
|
17
|
+
# @return [String, nil]
|
12
18
|
attr_reader :password
|
13
19
|
|
14
|
-
#
|
20
|
+
# The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
|
21
|
+
#
|
22
|
+
# @return [String, nil]
|
15
23
|
attr_reader :adapter_set
|
16
24
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
25
|
+
# If an error occurs on the stream connection that causes the session to terminate then details of the error will be
|
26
|
+
# stored in this attribute. If the session is terminated as a result of calling {#disconnect} then the error will be
|
27
|
+
# {SessionEndError}.
|
28
|
+
#
|
29
|
+
# @return [Error, nil]
|
20
30
|
attr_reader :error
|
21
31
|
|
22
32
|
# Initializes this new Lightstreamer session with the passed options.
|
23
33
|
#
|
24
34
|
# @param [Hash] options The options to create the session with.
|
25
35
|
# @option options [String] :server_url The URL of the Lightstreamer server. Required.
|
26
|
-
# @option options [String] :username The username to connect to the server with.
|
27
|
-
# @option options [String] :password The password to connect to the server with.
|
28
|
-
# @option options [String] :adapter_set The name of the adapter set to request from the server.
|
36
|
+
# @option options [String] :username The username to connect to the server with.
|
37
|
+
# @option options [String] :password The password to connect to the server with.
|
38
|
+
# @option options [String] :adapter_set The name of the adapter set to request from the server.
|
29
39
|
def initialize(options = {})
|
30
40
|
@subscriptions = []
|
31
41
|
@subscriptions_mutex = Mutex.new
|
@@ -37,7 +47,7 @@ module Lightstreamer
|
|
37
47
|
end
|
38
48
|
|
39
49
|
# Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
|
40
|
-
#
|
50
|
+
# a {LightstreamerError} subclass will be raised.
|
41
51
|
def connect
|
42
52
|
return if @stream_connection
|
43
53
|
|
@@ -74,15 +84,15 @@ module Lightstreamer
|
|
74
84
|
# Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
|
75
85
|
# stream connection be initiated by the client. The Lightstreamer server requires closure and re-establishment of
|
76
86
|
# 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
|
87
|
+
# requested explicitly by the client. If an error occurs then a {LightstreamerError} subclass will be raised.
|
78
88
|
def force_rebind
|
79
89
|
return unless @stream_connection
|
80
90
|
|
81
91
|
@control_connection.execute :force_rebind
|
82
92
|
end
|
83
93
|
|
84
|
-
# Subscribes this Lightstreamer session to the specified subscription. If an error occurs then
|
85
|
-
# will be raised.
|
94
|
+
# Subscribes this Lightstreamer session to the specified subscription. If an error occurs then a
|
95
|
+
# {LightstreamerError} subclass will be raised.
|
86
96
|
#
|
87
97
|
# @param [Subscription] subscription The new subscription to subscribe to.
|
88
98
|
def subscribe(subscription)
|
@@ -1,18 +1,23 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
#
|
3
|
-
# makes it available for consumption
|
2
|
+
# Internal class used by {Session} that manages a long-running Lightstreamer connection and handles incoming streaming
|
3
|
+
# data on a separate thread and makes it available for consumption through {#read_line}.
|
4
|
+
#
|
5
|
+
# @private
|
4
6
|
class StreamConnection
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
# @return [String] The session ID returned from the server when this stream connection was initiated.
|
7
|
+
# The session ID returned from the server when this stream connection was initiated.
|
8
|
+
#
|
9
|
+
# @return [String, nil]
|
9
10
|
attr_reader :session_id
|
10
11
|
|
11
|
-
#
|
12
|
+
# The control address returned from the server when this stream connection was initiated.
|
13
|
+
#
|
14
|
+
# @return [String, nil]
|
12
15
|
attr_reader :control_address
|
13
16
|
|
14
|
-
#
|
15
|
-
#
|
17
|
+
# If an error occurs on the stream thread that causes the stream to disconnect then the error will be stored in this
|
18
|
+
# attribute.
|
19
|
+
#
|
20
|
+
# @return [Error, nil]
|
16
21
|
attr_reader :error
|
17
22
|
|
18
23
|
# Establishes a new stream connection using the authentication details from the passed session.
|
@@ -24,22 +29,26 @@ module Lightstreamer
|
|
24
29
|
|
25
30
|
@stream_create_url = URI.join(session.server_url, '/lightstreamer/create_session.txt').to_s
|
26
31
|
@stream_bind_url = URI.join(session.server_url, '/lightstreamer/bind_session.txt').to_s
|
32
|
+
|
33
|
+
@connect_result_mutex = Mutex.new
|
34
|
+
@connect_result_condition_variable = ConditionVariable.new
|
27
35
|
end
|
28
36
|
|
29
37
|
# Establishes a new stream connection using the authentication details from the session that was passed to
|
30
|
-
# {#initialize}. Raises
|
38
|
+
# {#initialize}. Raises a {LightstreamerError} subclass on failure.
|
31
39
|
def connect
|
32
40
|
return if @thread
|
33
|
-
@session_id = @error = nil
|
34
41
|
@queue.clear
|
35
42
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
until @session_id || @error
|
43
|
+
@connect_result_mutex.synchronize do
|
44
|
+
create_stream_thread
|
45
|
+
@connect_result_condition_variable.wait @connect_result_mutex
|
40
46
|
end
|
41
47
|
|
42
|
-
|
48
|
+
return unless @error
|
49
|
+
|
50
|
+
@thread = nil
|
51
|
+
raise @error
|
43
52
|
end
|
44
53
|
|
45
54
|
# Returns whether or not this stream connection is connected.
|
@@ -51,12 +60,12 @@ module Lightstreamer
|
|
51
60
|
|
52
61
|
# Disconnects this stream connection by shutting down the streaming thread.
|
53
62
|
def disconnect
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
63
|
+
return unless @thread
|
64
|
+
|
65
|
+
@thread.exit
|
66
|
+
@thread.join
|
58
67
|
|
59
|
-
@
|
68
|
+
@thread = nil
|
60
69
|
end
|
61
70
|
|
62
71
|
# Reads the next line of streaming data. If the streaming thread is alive then this method blocks the calling thread
|
@@ -94,11 +103,11 @@ module Lightstreamer
|
|
94
103
|
|
95
104
|
params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
|
96
105
|
|
97
|
-
Typhoeus::Request.new @stream_create_url, method: :post, params: params
|
106
|
+
Typhoeus::Request.new @stream_create_url, method: :post, connecttimeout: 15, params: params
|
98
107
|
end
|
99
108
|
|
100
109
|
def stream_bind_post_request
|
101
|
-
Typhoeus::Request.new @stream_bind_url, method: :post, params: { LS_session: @session_id }
|
110
|
+
Typhoeus::Request.new @stream_bind_url, method: :post, connecttimeout: 15, params: { LS_session: @session_id }
|
102
111
|
end
|
103
112
|
|
104
113
|
def connect_stream_and_process_data(request)
|
@@ -109,12 +118,19 @@ module Lightstreamer
|
|
109
118
|
buffer.process data, &method(:process_stream_line)
|
110
119
|
end
|
111
120
|
|
112
|
-
request.on_complete
|
113
|
-
@error = @header.error if @header
|
114
|
-
@error = RequestError.new(response.return_message, response.response_code) unless response.success?
|
115
|
-
end
|
116
|
-
|
121
|
+
request.on_complete(&method(:on_request_complete))
|
117
122
|
request.run
|
123
|
+
|
124
|
+
signal_connect_result_ready
|
125
|
+
end
|
126
|
+
|
127
|
+
def on_request_complete(response)
|
128
|
+
@error = @header.error if @header
|
129
|
+
@error = RequestError.new(response.return_message, response.response_code) unless response.success?
|
130
|
+
end
|
131
|
+
|
132
|
+
def signal_connect_result_ready
|
133
|
+
@connect_result_mutex.synchronize { @connect_result_condition_variable.signal }
|
118
134
|
end
|
119
135
|
|
120
136
|
def process_stream_line(line)
|
@@ -128,24 +144,23 @@ module Lightstreamer
|
|
128
144
|
def process_header_line(line)
|
129
145
|
return if @header.process_header_line line
|
130
146
|
|
131
|
-
@control_address = @header['ControlAddress']
|
132
147
|
@session_id = @header['SessionId']
|
148
|
+
@control_address = @header['ControlAddress']
|
149
|
+
@error = @header.error
|
150
|
+
|
151
|
+
signal_connect_result_ready
|
133
152
|
|
134
153
|
@header = nil
|
135
154
|
end
|
136
155
|
|
137
156
|
def process_body_line(line)
|
138
|
-
if line
|
157
|
+
if line =~ /^LOOP/
|
139
158
|
@loop = true
|
140
159
|
elsif line =~ /^END/
|
141
160
|
@error = SessionEndError.new line[4..-1]
|
142
|
-
elsif
|
161
|
+
elsif line !~ /^(PROBE|Preamble:.*)$/
|
143
162
|
@queue.push line
|
144
163
|
end
|
145
164
|
end
|
146
|
-
|
147
|
-
def ignore_line?(line)
|
148
|
-
line =~ /^(PROBE|Preamble:.*)$/
|
149
|
-
end
|
150
165
|
end
|
151
166
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
module Lightstreamer
|
2
|
-
#
|
3
|
-
# created or an existing session is bound to.
|
2
|
+
# Internal class used by {StreamConnection} that processes the contents of the header returned by the server when a
|
3
|
+
# new stream connection is created or an existing session is bound to.
|
4
|
+
#
|
5
|
+
# @private
|
4
6
|
class StreamConnectionHeader
|
5
|
-
#
|
6
|
-
#
|
7
|
+
# If there was an error in the header then this value will be set to the error instance that should be raised in
|
8
|
+
# response.
|
9
|
+
#
|
10
|
+
# @return [Error, nil]
|
7
11
|
attr_reader :error
|
8
12
|
|
9
13
|
def initialize
|
@@ -49,7 +53,7 @@ module Lightstreamer
|
|
49
53
|
end
|
50
54
|
|
51
55
|
def process_error
|
52
|
-
@error =
|
56
|
+
@error = LightstreamerError.build @lines[2], @lines[1]
|
53
57
|
true
|
54
58
|
end
|
55
59
|
|
@@ -64,7 +68,7 @@ module Lightstreamer
|
|
64
68
|
end
|
65
69
|
|
66
70
|
def process_unrecognized
|
67
|
-
@error =
|
71
|
+
@error = LightstreamerError.new @lines.join(' ')
|
68
72
|
true
|
69
73
|
end
|
70
74
|
end
|
@@ -1,45 +1,61 @@
|
|
1
1
|
module Lightstreamer
|
2
2
|
# Describes a subscription that can be bound to a {Session} in order to consume its streaming data. A subscription is
|
3
3
|
# described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous data
|
4
|
-
# callback using {#
|
5
|
-
#
|
4
|
+
# callback using {#on_data} or by polling using {#item_data}. Subscriptions start receiving data once they are
|
5
|
+
# attached to a session using {Session#subscribe}.
|
6
6
|
class Subscription
|
7
|
-
#
|
8
|
-
#
|
7
|
+
# The unique identification number of this subscription.
|
8
|
+
#
|
9
|
+
# @return [Fixnum]
|
9
10
|
attr_reader :id
|
10
11
|
|
11
|
-
#
|
12
|
+
# The names of the items to subscribe to.
|
13
|
+
#
|
14
|
+
# @return [Array]
|
12
15
|
attr_reader :items
|
13
16
|
|
14
|
-
#
|
17
|
+
# The names of the fields to subscribe to on the items.
|
18
|
+
#
|
19
|
+
# @return [Array]
|
15
20
|
attr_reader :fields
|
16
21
|
|
17
|
-
#
|
22
|
+
# The operation mode of this subscription.
|
23
|
+
#
|
24
|
+
# @return [:distinct, :merge]
|
18
25
|
attr_reader :mode
|
19
26
|
|
20
|
-
#
|
21
|
-
#
|
27
|
+
# The name of the data adapter from the Lightstreamer session's adapter set that should be used, or `nil` to use the
|
28
|
+
# default data adapter.
|
29
|
+
#
|
30
|
+
# @return [String, nil]
|
22
31
|
attr_reader :adapter
|
23
32
|
|
24
|
-
#
|
33
|
+
# The selector for table items.
|
34
|
+
#
|
35
|
+
# @return [String, nil]
|
25
36
|
attr_reader :selector
|
26
37
|
|
27
|
-
#
|
28
|
-
#
|
38
|
+
# The maximum number of updates this subscription should receive per second. If this is set to zero, which is the
|
39
|
+
# default, then there is no limit on the update frequency. If set to `:unfiltered` then unfiltered streaming will be
|
40
|
+
# used for this subscription and it is possible for overflows to occur (see {#on_overflow}).
|
41
|
+
#
|
42
|
+
# @return [Float, :unfiltered]
|
29
43
|
attr_reader :maximum_update_frequency
|
30
44
|
|
31
45
|
# Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
|
32
46
|
# {Session#subscribe} to activate the subscription on a Lightstreamer session.
|
33
47
|
#
|
34
48
|
# @param [Hash] options The options to create the subscription with.
|
35
|
-
# @option options [Array] :items The names of the items to subscribe to.
|
36
|
-
# @option options [Array] :fields The names of the fields to subscribe to on the items.
|
37
|
-
# @option options [:distinct, :merge] :mode The operation mode of this subscription.
|
49
|
+
# @option options [Array] :items The names of the items to subscribe to. Required.
|
50
|
+
# @option options [Array] :fields The names of the fields to subscribe to on the items. Required.
|
51
|
+
# @option options [:distinct, :merge] :mode The operation mode of this subscription. Required.
|
38
52
|
# @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
|
39
53
|
# should be used. If `nil` then the default data adapter will be used.
|
40
54
|
# @option options [String] :selector The selector for table items. Optional.
|
41
|
-
# @option options [Float] :maximum_update_frequency The maximum number of updates this subscription
|
42
|
-
# per second. Defaults to zero which means there is no limit on the update frequency.
|
55
|
+
# @option options [Float, :unfiltered] :maximum_update_frequency The maximum number of updates this subscription
|
56
|
+
# should receive per second. Defaults to zero which means there is no limit on the update frequency.
|
57
|
+
# If set to `:unfiltered` then unfiltered streaming will be used for this subscription and it is
|
58
|
+
# possible for overflows to occur (see {#on_overflow}).
|
43
59
|
def initialize(options)
|
44
60
|
@id = self.class.next_id
|
45
61
|
|
@@ -51,62 +67,55 @@ module Lightstreamer
|
|
51
67
|
@maximum_update_frequency = options[:maximum_update_frequency] || 0.0
|
52
68
|
|
53
69
|
@data_mutex = Mutex.new
|
54
|
-
clear_data
|
55
70
|
|
56
|
-
|
71
|
+
clear_data
|
72
|
+
clear_callbacks
|
57
73
|
end
|
58
74
|
|
59
75
|
# Clears all current data stored for this subscription. New data will continue to be processed as it becomes
|
60
76
|
# available.
|
61
77
|
def clear_data
|
62
|
-
@
|
63
|
-
@data = (0...items.size).map { { distinct: [], merge: {} }.fetch(mode) }
|
64
|
-
end
|
78
|
+
@data = (0...items.size).map { {} }
|
65
79
|
end
|
66
80
|
|
67
|
-
#
|
68
|
-
#
|
81
|
+
# Adds the passed block to the list of callbacks that will be run when new data for this subscription arrives. The
|
82
|
+
# block will be called on a worker thread and so the code that is run by the block must be thread-safe. The
|
83
|
+
# arguments passed to the block are `|subscription, item_name, item_data, new_values|`. If {#mode} is `:distinct`
|
84
|
+
# then the values of `item_data` and `new_values` will be the same.
|
69
85
|
#
|
70
|
-
# @param [
|
71
|
-
def
|
72
|
-
index = @items.index item_name
|
73
|
-
raise ArgumentError, 'Unrecognized item name' unless index
|
74
|
-
|
86
|
+
# @param [Proc] callback The callback that is to be run when new data arrives.
|
87
|
+
def on_data(&callback)
|
75
88
|
@data_mutex.synchronize do
|
76
|
-
@
|
89
|
+
@callbacks[:on_data] << callback
|
77
90
|
end
|
78
91
|
end
|
79
92
|
|
80
|
-
# Adds the passed block to the list of callbacks that will be run when
|
81
|
-
# block will be called on a worker thread and so the code that is run by the block must be
|
82
|
-
# arguments passed to the block are `|subscription, item_name,
|
83
|
-
#
|
84
|
-
# @param [Proc] block The callback block that is to be run when new data arrives.
|
93
|
+
# Adds the passed block to the list of callbacks that will be run when the server reports an overflow for this
|
94
|
+
# subscription. The block will be called on a worker thread and so the code that is run by the block must be
|
95
|
+
# thread-safe. The arguments passed to the block are `|subscription, item_name, overflow_size|`.
|
85
96
|
#
|
86
|
-
# @
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
block
|
97
|
+
# @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
|
98
|
+
def on_overflow(&callback)
|
99
|
+
@data_mutex.synchronize do
|
100
|
+
@callbacks[:on_overflow] << callback
|
101
|
+
end
|
92
102
|
end
|
93
103
|
|
94
|
-
# Removes
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
104
|
+
# Removes all {#on_data} and {#on_overflow} callbacks present on this subscription.
|
105
|
+
def clear_callbacks
|
106
|
+
@data_mutex.synchronize do
|
107
|
+
@callbacks = { on_data: [], on_overflow: [] }
|
108
|
+
end
|
99
109
|
end
|
100
110
|
|
101
|
-
# Returns the current data of one of this subscription's items.
|
111
|
+
# Returns a copy of the current data of one of this subscription's items.
|
102
112
|
#
|
103
113
|
# @param [String] item_name The name of the item to return the current data for.
|
104
114
|
#
|
105
|
-
# @return [Hash
|
106
|
-
|
107
|
-
def retrieve_item_data(item_name)
|
115
|
+
# @return [Hash] A copy of the item data.
|
116
|
+
def item_data(item_name)
|
108
117
|
index = @items.index item_name
|
109
|
-
raise ArgumentError, '
|
118
|
+
raise ArgumentError, 'Unknown item' unless index
|
110
119
|
|
111
120
|
@data_mutex.synchronize do
|
112
121
|
@data[index].dup
|
@@ -120,25 +129,17 @@ module Lightstreamer
|
|
120
129
|
#
|
121
130
|
# @return [Boolean] Whether the passed line of stream data was relevant to this subscription and was successfully
|
122
131
|
# processed by it.
|
132
|
+
#
|
133
|
+
# @private
|
123
134
|
def process_stream_data(line)
|
124
|
-
|
125
|
-
return false unless item_index
|
126
|
-
|
127
|
-
@data_mutex.synchronize do
|
128
|
-
data = @data[item_index]
|
129
|
-
|
130
|
-
data << new_values if mode == :distinct
|
131
|
-
data.merge!(new_values) if mode == :merge
|
132
|
-
|
133
|
-
call_data_callbacks @items[item_index], data, new_values
|
134
|
-
end
|
135
|
-
|
136
|
-
true
|
135
|
+
process_update_message(line) || process_overflow_message(line)
|
137
136
|
end
|
138
137
|
|
139
|
-
# Returns the next unique ID
|
138
|
+
# Returns the next unique subscription ID.
|
140
139
|
#
|
141
140
|
# @return [Fixnum]
|
141
|
+
#
|
142
|
+
# @private
|
142
143
|
def self.next_id
|
143
144
|
@next_id ||= 0
|
144
145
|
@next_id += 1
|
@@ -146,57 +147,40 @@ module Lightstreamer
|
|
146
147
|
|
147
148
|
private
|
148
149
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
match = line.match stream_data_regex
|
153
|
-
return unless match
|
154
|
-
|
155
|
-
item_index = match.captures[0].to_i - 1
|
156
|
-
return unless item_index < @items.size
|
150
|
+
def process_update_message(line)
|
151
|
+
update_message = UpdateMessage.parse line, id, items, fields
|
152
|
+
return unless update_message
|
157
153
|
|
158
|
-
|
159
|
-
|
154
|
+
@data_mutex.synchronize do
|
155
|
+
process_new_values update_message.item_index, update_message.values
|
156
|
+
end
|
160
157
|
|
161
|
-
|
162
|
-
# this subscription. The ID at the beginning must match, as well as the number of fields.
|
163
|
-
def stream_data_regex
|
164
|
-
Regexp.new "^#{id},(\\d+)#{'\|(.*)' * fields.size}"
|
158
|
+
true
|
165
159
|
end
|
166
160
|
|
167
|
-
|
168
|
-
|
169
|
-
def parse_values(values)
|
170
|
-
hash = {}
|
161
|
+
def process_new_values(item_index, new_values)
|
162
|
+
data = @data[item_index]
|
171
163
|
|
172
|
-
|
173
|
-
|
164
|
+
data.replace(new_values) if mode == :distinct
|
165
|
+
data.merge!(new_values) if mode == :merge
|
174
166
|
|
175
|
-
|
176
|
-
end
|
177
|
-
|
178
|
-
hash
|
167
|
+
run_callbacks :on_data, @items[item_index], data, new_values
|
179
168
|
end
|
180
169
|
|
181
|
-
|
182
|
-
|
183
|
-
return
|
184
|
-
return nil if value == '#'
|
170
|
+
def process_overflow_message(line)
|
171
|
+
overflow_message = OverflowMessage.parse line, id, items
|
172
|
+
return unless overflow_message
|
185
173
|
|
186
|
-
|
174
|
+
@data_mutex.synchronize do
|
175
|
+
run_callbacks :on_overflow, @items[overflow_message.item_index], overflow_message.overflow_size
|
176
|
+
end
|
187
177
|
|
188
|
-
|
178
|
+
true
|
189
179
|
end
|
190
180
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
@data_callbacks.each do |callback|
|
195
|
-
begin
|
196
|
-
callback.call self, item_name, item_data, new_values
|
197
|
-
rescue StandardError => error
|
198
|
-
warn "Lightstreamer: exception occurred in a subscription data callback: #{error}"
|
199
|
-
end
|
181
|
+
def run_callbacks(callback_type, *arguments)
|
182
|
+
@callbacks.fetch(callback_type).each do |callback|
|
183
|
+
callback.call self, *arguments
|
200
184
|
end
|
201
185
|
end
|
202
186
|
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.
|
4
|
+
version: '0.5'
|
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-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -181,11 +181,12 @@ files:
|
|
181
181
|
- lib/lightstreamer/control_connection.rb
|
182
182
|
- lib/lightstreamer/errors.rb
|
183
183
|
- lib/lightstreamer/line_buffer.rb
|
184
|
+
- lib/lightstreamer/messages/overflow_message.rb
|
185
|
+
- lib/lightstreamer/messages/update_message.rb
|
184
186
|
- lib/lightstreamer/session.rb
|
185
187
|
- lib/lightstreamer/stream_connection.rb
|
186
188
|
- lib/lightstreamer/stream_connection_header.rb
|
187
189
|
- lib/lightstreamer/subscription.rb
|
188
|
-
- lib/lightstreamer/utf16.rb
|
189
190
|
- lib/lightstreamer/version.rb
|
190
191
|
homepage: https://github.com/rviney/lightstreamer
|
191
192
|
licenses:
|
data/lib/lightstreamer/utf16.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
module Lightstreamer
|
2
|
-
# This module supports the decoding of UTF-16 escape sequences
|
3
|
-
module UTF16
|
4
|
-
module_function
|
5
|
-
|
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]
|
12
|
-
def decode_escape_sequences(string)
|
13
|
-
string = decode_surrogate_pairs_escape_sequences string
|
14
|
-
|
15
|
-
# Match all escape sequences
|
16
|
-
string.gsub(/\\u[A-F\d]{4}/i) do |escape_sequence|
|
17
|
-
codepoint = escape_sequence[2..-1].hex
|
18
|
-
|
19
|
-
# Codepoints greater than 0xD7FF are invalid are ignored
|
20
|
-
codepoint < 0xD800 ? [codepoint].pack('U') : ''
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
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]
|
29
|
-
def decode_surrogate_pairs_escape_sequences(string)
|
30
|
-
string.gsub(/\\uD[89AB][A-F\d]{2}\\uD[C-F][A-F\d]{2}/i) do |escape_sequence|
|
31
|
-
high_surrogate = escape_sequence[2...6].hex
|
32
|
-
low_surrogate = escape_sequence[8...12].hex
|
33
|
-
|
34
|
-
codepoint = 0x10000 + ((high_surrogate - 0xD800) << 10) + (low_surrogate - 0xDC00)
|
35
|
-
|
36
|
-
[codepoint].pack 'U'
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|