lightstreamer 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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