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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 863ad0ba9936b00dca802004782358a03cf5cd36
4
- data.tar.gz: a13adc3e4b6b9945580bc02927826d61742dba1d
3
+ metadata.gz: 3c64dd0d680841a38eb530d0a17fa0700985426a
4
+ data.tar.gz: 5a66944aff7e81aeef70c8cb5551703c1389c281
5
5
  SHA512:
6
- metadata.gz: 7f9dd91cc66467027ddd209d0c964559db461e8b9a05c1cb9aa583ae42d1997516c0196ea2be18c0fbc5d317db97fcef10ce81e32cefe677390dd4c8f339ebfc
7
- data.tar.gz: 4863842643a0d7a876f6efa2b41270044f7ada1479c0154e452e02f7e1d6b2e75c39f6f24d79029b633dd7402c3cd6a5d24a41e5c3dba057013a0ddc403b9dbc
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.add_data_callback do |subscription, item_name, item_data, new_values|
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, type: :numeric, desc: 'The maximum number of updates per second for each item'
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.add_data_callback(&method(:subscription_data_callback))
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 subscription_data_callback(_subscription, item_name, _item_data, new_values)
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
- # This is an internal class used by {Session} and is responsible for sending Lightstreamer control requests.
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 an {Error} subclass will be raised.
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 Error.build(result[2], result[1]) if result.first != 'OK'
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 an {Error} subclass will be raised.
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
 
@@ -1,114 +1,120 @@
1
1
  module Lightstreamer
2
- # Base class for all errors raised by this gem.
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
44
+ class InvalidDataAdapterError < LightstreamerError
46
45
  end
47
46
 
48
47
  # This error occurs when the specified table is not found.
49
- class UnknownTableError < Error
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 < Error
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 < Error
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 < Error
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 < Error
64
+ class UnsupportedModeForItemError < LightstreamerError
66
65
  end
67
66
 
68
67
  # This error is raised when an invalid selector is specified.
69
- class InvalidSelectorError < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
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 < Error
102
- # @return [String] The error message from the metadata adapter.
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
- # @return [Fixnum] The error code from the metadata adapter.
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.to_i
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 < Error
125
+ class SyncError < LightstreamerError
120
126
  end
121
127
 
122
- # This error is raised when the specified session ID is for a session that has been terminated.
123
- class SessionEndError < Error
124
- # @return [Fixnum] The cause code specifying why the session was terminated by the server, or `nil` if unknown.
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 [Fixnum] cause_code
130
- def initialize(cause_code = nil)
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 < Error
138
- # @return [String] A description of the request error that occurred.
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
- # @return [Fixnum] The HTTP code that was returned, or zero if unknown.
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 [Integer] code The HTTP code for the request failure, if known.
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.to_i
174
+ @request_error_code = code
151
175
 
152
- super "Request error #{code}: #{message}"
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 Error
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 {Error} subclass to instantiate.
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 as they become complete.
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. Any lines that are now complete will be yielded back.
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
- # @return [String] The URL of the Lightstreamer server to connect to. Set by {#initialize}.
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
- # @return [String] The username to connect to the Lightstreamer server with. Set by {#initialize}.
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
- # @return [String] The password to connect to the Lightstreamer server with. Set by {#initialize}.
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
- # @return [String] The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
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
- # @return [Error] If an error occurs on the stream connection that causes the session to terminate then details of
18
- # the error will be stored in this attribute. If the session is terminated as a result of calling
19
- # {#disconnect} then the error will be {SessionEndError}.
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. Optional.
27
- # @option options [String] :password The password to connect to the server with. Optional.
28
- # @option options [String] :adapter_set The name of the adapter set to request from the server. Optional.
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
- # an {Error} subclass will be raised.
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 an {Error} subclass will be raised.
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 an {Error} subclass
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
- # Manages a long-running Lightstreamer connection that handles incoming streaming data on a separate thread and
3
- # makes it available for consumption via the {#read_line} method.
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
- # @return [Thread] The thread used to process incoming streaming data.
6
- attr_reader :thread
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
- # @return [String] The control address returned from the server when this stream connection was initiated.
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
- # @return [Error] If an error occurs on the stream thread that causes the stream to disconnect then the
15
- # error will be stored in this attribute.
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 an {Error} subclass on failure.
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
- create_stream_thread
37
-
38
- # Wait until the connection result is known
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
- raise @error if @error
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
- if @thread
55
- @thread.exit
56
- @thread.join
57
- end
63
+ return unless @thread
64
+
65
+ @thread.exit
66
+ @thread.join
58
67
 
59
- @session_id = @control_address = @error = @thread = nil
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 do |response|
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 == 'LOOP'
157
+ if line =~ /^LOOP/
139
158
  @loop = true
140
159
  elsif line =~ /^END/
141
160
  @error = SessionEndError.new line[4..-1]
142
- elsif !ignore_line?(line)
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
- # Helper class that processes the contents of the header returned by the server when a new stream connection is
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
- # @return [Error] If there was an error in the header then this value will be set to the error instance that should
6
- # be raised in response.
7
+ # 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 = Error.build @lines[2], @lines[1]
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 = Error.new @lines.join(' ')
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 {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data only
5
- # once they are attached to a session using {Session#subscribe}.
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
- # @return [Fixnum] The unique identification number of this subscription. This is used to identify the subscription
8
- # in incoming Lightstreamer data.
7
+ # The unique identification number of this subscription.
8
+ #
9
+ # @return [Fixnum]
9
10
  attr_reader :id
10
11
 
11
- # @return [Array] The names of the items to subscribe to.
12
+ # The names of the items to subscribe to.
13
+ #
14
+ # @return [Array]
12
15
  attr_reader :items
13
16
 
14
- # @return [Array] The names of the fields to subscribe to on the items.
17
+ # The names of the fields to subscribe to on the items.
18
+ #
19
+ # @return [Array]
15
20
  attr_reader :fields
16
21
 
17
- # @return [:distinct, :merge] The operation mode of this subscription.
22
+ # The operation mode of this subscription.
23
+ #
24
+ # @return [:distinct, :merge]
18
25
  attr_reader :mode
19
26
 
20
- # @return [String] The name of the data adapter from the Lightstreamer session's adapter set that should be used.
21
- # If `nil` then the default data adapter will be used.
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
- # @return [String] The selector for table items. Optional.
33
+ # The selector for table items.
34
+ #
35
+ # @return [String, nil]
25
36
  attr_reader :selector
26
37
 
27
- # @return [Float] The maximum number of updates this subscription should receive per second. If this is set to zero,
28
- # which is the default, then there is no limit on the update frequency.
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 should receive
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
- @data_callbacks = []
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
- @data_mutex.synchronize do
63
- @data = (0...items.size).map { { distinct: [], merge: {} }.fetch(mode) }
64
- end
78
+ @data = (0...items.size).map { {} }
65
79
  end
66
80
 
67
- # Clears the current data stored for the specified item. This is important to do when {#mode} is `:distinct` as
68
- # otherwise the incoming data will build up indefinitely.
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 [String] item_name The name of the item to clear the current data for.
71
- def clear_data_for_item(item_name)
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
- @data[index] = { distinct: [], merge: {} }.fetch(mode)
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 new data for this subscription arrives. The
81
- # block will be called on a worker thread and so the code that is run by the block must be thread-safe. The
82
- # arguments passed to the block are `|subscription, item_name, item_data, new_values|`.
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
- # @return [Proc] The same `Proc` object that was passed to this method. This can be used to remove this data
87
- # callback at a later stage using {#remove_data_callback}.
88
- def add_data_callback(&block)
89
- @data_mutex.synchronize { @data_callbacks << block }
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 a data callback that was added by {#add_data_callback}.
95
- #
96
- # @param [Proc] block The data callback block to remove.
97
- def remove_data_callback(block)
98
- @data_mutex.synchronize { @data_callbacks.delete block }
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, Array] The item data. Will be a `Hash` if {#mode} is `:merge`, and an `Array` if {#mode} is
106
- # `:distinct`.
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, 'Unrecognized item name' unless index
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
- item_index, new_values = parse_stream_data line
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 to use for a new subscription.
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
- # Attempts to parse a line of stream data. If parsing is successful then the first return value is the item index,
150
- # and the second is a hash of the values contained in the stream data.
151
- def parse_stream_data(line)
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
- [item_index, parse_values(match.captures[1..-1])]
159
- end
154
+ @data_mutex.synchronize do
155
+ process_new_values update_message.item_index, update_message.values
156
+ end
160
157
 
161
- # Returns the regular expression that will match a single line of data in the incoming stream that is relevant to
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
- # Parses an array of values from an incoming line of stream data into a hash where the keys are the field names
168
- # defined for this subscription.
169
- def parse_values(values)
170
- hash = {}
161
+ def process_new_values(item_index, new_values)
162
+ data = @data[item_index]
171
163
 
172
- values.each_with_index do |value, index|
173
- next if value == ''
164
+ data.replace(new_values) if mode == :distinct
165
+ data.merge!(new_values) if mode == :merge
174
166
 
175
- hash[fields[index]] = parse_raw_field_value value
176
- end
177
-
178
- hash
167
+ run_callbacks :on_data, @items[item_index], data, new_values
179
168
  end
180
169
 
181
- # Parses a raw field value according to to the Lightstreamer specification.
182
- def parse_raw_field_value(value)
183
- return '' if value == '$'
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
- value = value[1..-1] if value =~ /^(\$|#)/
174
+ @data_mutex.synchronize do
175
+ run_callbacks :on_overflow, @items[overflow_message.item_index], overflow_message.overflow_size
176
+ end
187
177
 
188
- UTF16.decode_escape_sequences value
178
+ true
189
179
  end
190
180
 
191
- # Invokes all of this subscription's data callbacks with the specified arguments. Any exceptions that occur in a
192
- # data callback are reported on `stderr` but are otherwise ignored.
193
- def call_data_callbacks(item_name, item_data, new_values)
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
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.4'.freeze
3
+ VERSION = '0.5'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lightstreamer
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.4'
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-25 00:00:00.000000000 Z
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:
@@ -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