lightstreamer 0.4 → 0.5
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 +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
|