lightstreamer 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +24 -10
- data/lib/lightstreamer.rb +1 -0
- data/lib/lightstreamer/cli/commands/stream_command.rb +9 -6
- data/lib/lightstreamer/errors.rb +1 -1
- data/lib/lightstreamer/messages/send_message_outcome_message.rb +2 -2
- data/lib/lightstreamer/messages/update_message.rb +7 -7
- data/lib/lightstreamer/session.rb +40 -20
- data/lib/lightstreamer/stream_connection.rb +22 -10
- data/lib/lightstreamer/subscription.rb +72 -65
- data/lib/lightstreamer/subscription_item_data.rb +93 -0
- data/lib/lightstreamer/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fbedc6ddd408cf66740c82acb37a83fb1954b13
|
4
|
+
data.tar.gz: e259733aa9ab2bd100a1cee9a58ea120de44614c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a47999d9791bf4eb11671fadf6ed949ff3bed475d7856ce2ae1a9f99dceba78608b52ac630ba58a5425a1154c6807ab9aa7e2937119f3e8136051a81be82be84
|
7
|
+
data.tar.gz: 689b06550c0ff39e08ec1042a80e6aa99ab47ddeaa92c0acd6f064eef6bcb6e9be202b90a19627c878157e3d41c795e1b131245ed0538364faf2e15d9dfff5cf
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Lightstreamer Changelog
|
2
2
|
|
3
|
+
### 0.8 — August 2, 2016
|
4
|
+
|
5
|
+
- Added support for Lightstreamer's `COMMAND` and `RAW` subscription modes
|
6
|
+
- Added support for Lightstreamer's polling mode and switching an active session between streaming and polling
|
7
|
+
- `Lightstreamer::Subscription#item_data` now return `nil` if the requested item has never had any data set
|
8
|
+
- Renamed `Lightstreamer::Subscription#adapter` to `Lightstreamer::Subscription#data_adapter` and the `--adapter`
|
9
|
+
command-line option to `--data-adapter`
|
10
|
+
- The `--mode` command-line option is now required because the default value of `merge` has been removed
|
11
|
+
|
3
12
|
### 0.7 — July 31, 2016
|
4
13
|
|
5
14
|
- Refactored subscription handling to be more object-oriented, subscriptions are now created using
|
data/README.md
CHANGED
@@ -8,9 +8,22 @@
|
|
8
8
|
[![Documentation][documentation-badge]][documentation-link]
|
9
9
|
[![License][license-badge]][license-link]
|
10
10
|
|
11
|
-
Easily interface with a Lightstreamer service from Ruby
|
11
|
+
Easily interface with a Lightstreamer service from Ruby with this gem, either directly through code or by using the
|
12
|
+
provided command-line client. Written against the
|
12
13
|
[official API specification](http://www.lightstreamer.com/docs/client_generic_base/Network%20Protocol%20Tutorial.pdf).
|
13
14
|
|
15
|
+
Includes support for:
|
16
|
+
|
17
|
+
- Streaming and polling connections
|
18
|
+
- The four Lightstreamer subscription modes: `command`, `distinct`, `merge` and `raw`
|
19
|
+
- Automatic management of table content when in `command` mode
|
20
|
+
- Silent subscriptions
|
21
|
+
- Item snapshots
|
22
|
+
- Unfiltered subscriptions and asynchronous overflow handling
|
23
|
+
- Bulk subscription creation
|
24
|
+
- Synchronous and asynchronous message sending
|
25
|
+
- Detailed error reporting and error handling callbacks
|
26
|
+
|
14
27
|
## License
|
15
28
|
|
16
29
|
Licensed under the MIT license. You must read and agree to its terms to use this software.
|
@@ -30,8 +43,8 @@ The two primary classes that make up the public API are:
|
|
30
43
|
- [`Lightstreamer::Session`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Session)
|
31
44
|
- [`Lightstreamer::Subscription`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Subscription)
|
32
45
|
|
33
|
-
The following code snippet demonstrates how to
|
34
|
-
output as it
|
46
|
+
The following code snippet demonstrates how to create a Lightstreamer session, build a subscription, then print
|
47
|
+
streaming output as it arrives.
|
35
48
|
|
36
49
|
```ruby
|
37
50
|
require 'lightstreamer'
|
@@ -46,19 +59,19 @@ session.connect
|
|
46
59
|
# Create a new subscription that subscribes to thirty items and to four fields on each item
|
47
60
|
subscription = session.build_subscription items: (1..30).map { |i| "item#{i}" },
|
48
61
|
fields: [:ask, :bid, :stock_name, :time],
|
49
|
-
mode: :merge,
|
62
|
+
mode: :merge, data_adapter: 'QUOTE_ADAPTER'
|
50
63
|
|
51
64
|
# Create a thread-safe queue
|
52
65
|
queue = Queue.new
|
53
66
|
|
54
67
|
# When new data becomes available for the subscription it will be put on the queue. This callback
|
55
68
|
# will be run on a worker thread.
|
56
|
-
subscription.on_data do |subscription, item_name, item_data,
|
69
|
+
subscription.on_data do |subscription, item_name, item_data, new_data|
|
57
70
|
queue.push item_data
|
58
71
|
end
|
59
72
|
|
60
|
-
# Start streaming data for the subscription
|
61
|
-
subscription.start
|
73
|
+
# Start streaming data for the subscription and request an initial snapshot
|
74
|
+
subscription.start snapshot: true
|
62
75
|
|
63
76
|
# Loop printing out new data as soon as it becomes available on the queue
|
64
77
|
loop do
|
@@ -75,11 +88,12 @@ subscription, then print streaming output from the server as it becomes availabl
|
|
75
88
|
To print streaming data from the demo server run the following command:
|
76
89
|
|
77
90
|
```
|
78
|
-
lightstreamer --server-url http://push.lightstreamer.com --adapter-set DEMO
|
79
|
-
--
|
91
|
+
lightstreamer --server-url http://push.lightstreamer.com --adapter-set DEMO \
|
92
|
+
--data-adapter QUOTE_ADAPTER --mode merge --snapshot \
|
93
|
+
--items item1 item2 item3 item4 item5 --fields ask bid stock_name
|
80
94
|
```
|
81
95
|
|
82
|
-
To see
|
96
|
+
To see the full list of available options for the command-line client run the following command:
|
83
97
|
|
84
98
|
```
|
85
99
|
lightstreamer help stream
|
data/lib/lightstreamer.rb
CHANGED
@@ -15,6 +15,7 @@ require 'lightstreamer/stream_buffer'
|
|
15
15
|
require 'lightstreamer/stream_connection'
|
16
16
|
require 'lightstreamer/stream_connection_header'
|
17
17
|
require 'lightstreamer/subscription'
|
18
|
+
require 'lightstreamer/subscription_item_data'
|
18
19
|
require 'lightstreamer/version'
|
19
20
|
|
20
21
|
# This module contains all the code for the Lightstreamer gem. See `README.md` to get started with using this gem.
|
@@ -8,11 +8,13 @@ module Lightstreamer
|
|
8
8
|
option :username, desc: 'The username for the session'
|
9
9
|
option :password, desc: 'The password for the session'
|
10
10
|
option :adapter_set, desc: 'The name of the adapter set for the session'
|
11
|
+
option :polling_enabled, type: :boolean, desc: 'Whether to poll instead of using long-running stream connections'
|
11
12
|
option :requested_maximum_bandwidth, type: :numeric, desc: 'The requested maximum bandwidth, in kbps'
|
12
|
-
|
13
|
+
|
14
|
+
option :data_adapter, desc: 'The name of the data adapter to stream data from'
|
13
15
|
option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
|
14
16
|
option :fields, type: :array, required: true, desc: 'The field(s) to stream'
|
15
|
-
option :mode, enum: %w(distinct merge),
|
17
|
+
option :mode, enum: %w(command distinct merge raw), desc: 'The operation mode'
|
16
18
|
option :selector, desc: 'The selector for table items'
|
17
19
|
option :snapshot, type: :boolean, desc: 'Whether to send snapshot data for the items'
|
18
20
|
option :maximum_update_frequency, desc: 'The maximum number of updates per second for each item'
|
@@ -52,17 +54,18 @@ module Lightstreamer
|
|
52
54
|
|
53
55
|
def session_options
|
54
56
|
{ server_url: options[:server_url], username: options[:username], password: options[:password],
|
55
|
-
adapter_set: options[:adapter_set], requested_maximum_bandwidth: options[:requested_maximum_bandwidth]
|
57
|
+
adapter_set: options[:adapter_set], requested_maximum_bandwidth: options[:requested_maximum_bandwidth],
|
58
|
+
polling_enabled: options[:polling_enabled] }
|
56
59
|
end
|
57
60
|
|
58
61
|
def subscription_options
|
59
|
-
{ items: options[:items], fields: options[:fields], mode: options[:mode],
|
62
|
+
{ items: options[:items], fields: options[:fields], mode: options[:mode], data_adapter: options[:data_adapter],
|
60
63
|
maximum_update_frequency: options[:maximum_update_frequency], selector: options[:selector],
|
61
64
|
snapshot: options[:snapshot] }
|
62
65
|
end
|
63
66
|
|
64
|
-
def on_data(_subscription, item_name, _item_data,
|
65
|
-
@queue.push "#{item_name} - #{
|
67
|
+
def on_data(_subscription, item_name, _item_data, new_data)
|
68
|
+
@queue.push "#{item_name} - #{new_data.map { |key, value| "#{key}: #{value}" }.join ', '}"
|
66
69
|
end
|
67
70
|
|
68
71
|
def on_overflow(_subscription, item_name, overflow_size)
|
data/lib/lightstreamer/errors.rb
CHANGED
@@ -161,7 +161,7 @@ module Lightstreamer
|
|
161
161
|
|
162
162
|
# Initializes this session end error with the specified cause code.
|
163
163
|
#
|
164
|
-
# @param [
|
164
|
+
# @param [String, Fixnum, nil] cause_code See {#cause_code} for details.
|
165
165
|
def initialize(cause_code)
|
166
166
|
@cause_code = cause_code && cause_code.to_i
|
167
167
|
super()
|
@@ -9,8 +9,8 @@ module Lightstreamer
|
|
9
9
|
attr_accessor :sequence
|
10
10
|
|
11
11
|
# The message number(s) this message outcome is for. There will always be exactly one entry in this array except in
|
12
|
-
# the case where {#error} is a {MessagesSkippedByTimeoutError} in which case there may be more than one
|
13
|
-
# multiple messages were skipped.
|
12
|
+
# the case where {#error} is a {Errors::MessagesSkippedByTimeoutError} in which case there may be more than one
|
13
|
+
# entry if multiple messages were skipped.
|
14
14
|
#
|
15
15
|
# @return [Array<Fixnum>]
|
16
16
|
attr_accessor :numbers
|
@@ -8,10 +8,10 @@ module Lightstreamer
|
|
8
8
|
# @return [Fixnum]
|
9
9
|
attr_accessor :item_index
|
10
10
|
|
11
|
-
# The field
|
11
|
+
# The field data specified by this update message.
|
12
12
|
#
|
13
13
|
# @return [Array]
|
14
|
-
attr_accessor :
|
14
|
+
attr_accessor :data
|
15
15
|
|
16
16
|
class << self
|
17
17
|
# Attempts to parses the specified line as an update message for the given table, items, and fields, and returns
|
@@ -25,7 +25,7 @@ module Lightstreamer
|
|
25
25
|
message.item_index = match.captures[0].to_i - 1
|
26
26
|
return unless message.item_index < items.size
|
27
27
|
|
28
|
-
message.
|
28
|
+
message.data = parse_field_values match.captures[1..-1], fields
|
29
29
|
|
30
30
|
message
|
31
31
|
end
|
@@ -36,13 +36,13 @@ module Lightstreamer
|
|
36
36
|
Regexp.new "^#{table_id},(\\d+)#{'\|(.*)' * fields.size}$"
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
39
|
+
def parse_field_values(field_values, fields)
|
40
40
|
hash = {}
|
41
41
|
|
42
|
-
|
43
|
-
next if
|
42
|
+
field_values.each_with_index do |field_value, index|
|
43
|
+
next if field_value == ''
|
44
44
|
|
45
|
-
hash[fields[index]] = parse_raw_field_value
|
45
|
+
hash[fields[index]] = parse_raw_field_value field_value
|
46
46
|
end
|
47
47
|
|
48
48
|
hash
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Lightstreamer
|
2
2
|
# This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class forms the
|
3
|
-
# primary API for working with Lightstreamer.
|
3
|
+
# primary API for working with Lightstreamer. Start by calling {#initialize} with the desired server URL and other
|
4
|
+
# options, then call {#connect} to initiate the session. Once connected create subscriptions using
|
5
|
+
# {#build_subscription} and then start streaming data by calling {Subscription#start} or {#bulk_subscription_start}.
|
6
|
+
# See the {Subscription} class for details on how to consume the streaming data as it arrives.
|
4
7
|
class Session
|
5
8
|
# The URL of the Lightstreamer server to connect to. Set by {#initialize}.
|
6
9
|
#
|
@@ -34,6 +37,15 @@ module Lightstreamer
|
|
34
37
|
# @return [Float]
|
35
38
|
attr_reader :requested_maximum_bandwidth
|
36
39
|
|
40
|
+
# Whether polling mode is enabled. By default long-running HTTP connections will be used to stream incoming data,
|
41
|
+
# but if polling is enabled then repeated short polling requests will be used instead. Polling may work better if
|
42
|
+
# there is intermediate buffering on the network that affects timely delivery of data on long-running streaming
|
43
|
+
# connections. The polling mode for a connected session can be changed by setting {#polling_enabled} and then
|
44
|
+
# calling {#force_rebind}.
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
attr_accessor :polling_enabled
|
48
|
+
|
37
49
|
# Initializes this new Lightstreamer session with the passed options.
|
38
50
|
#
|
39
51
|
# @param [Hash] options The options to create the session with.
|
@@ -43,6 +55,8 @@ module Lightstreamer
|
|
43
55
|
# @option options [String] :adapter_set The name of the adapter set to request from the server.
|
44
56
|
# @option options [Float] :requested_maximum_bandwidth. The server-side bandwidth constraint on data usage,
|
45
57
|
# expressed in kbps. Defaults to zero which means no limit is applied.
|
58
|
+
# @option options [Boolean] :polling_enabled Whether polling mode is enabled. See {#polling_enabled} for details.
|
59
|
+
# Defaults to `false`.
|
46
60
|
def initialize(options = {})
|
47
61
|
@subscriptions = []
|
48
62
|
@subscriptions_mutex = Mutex.new
|
@@ -52,6 +66,7 @@ module Lightstreamer
|
|
52
66
|
@password = options[:password]
|
53
67
|
@adapter_set = options[:adapter_set]
|
54
68
|
@requested_maximum_bandwidth = options[:requested_maximum_bandwidth].to_f
|
69
|
+
@polling_enabled = options[:polling_enabled]
|
55
70
|
|
56
71
|
@on_message_result_callbacks = []
|
57
72
|
end
|
@@ -105,7 +120,9 @@ module Lightstreamer
|
|
105
120
|
# Requests that the Lightstreamer server terminate the currently active stream connection and require that a new
|
106
121
|
# stream connection be initiated by the client. The Lightstreamer server requires closure and re-establishment of
|
107
122
|
# the stream connection periodically during normal operation, this method just allows such a reconnection to be
|
108
|
-
# requested explicitly by the client.
|
123
|
+
# requested explicitly by the client. This is particularly useful after {#polling_enabled} has been changed because
|
124
|
+
# it forces the stream connection to rebind using the new setting. If an error occurs then a {LightstreamerError}
|
125
|
+
# subclass will be raised.
|
109
126
|
def force_rebind
|
110
127
|
return unless @stream_connection
|
111
128
|
|
@@ -119,9 +136,9 @@ module Lightstreamer
|
|
119
136
|
# @param [Hash] options The options to create the subscription with.
|
120
137
|
# @option options [Array] :items The names of the items to subscribe to. Required.
|
121
138
|
# @option options [Array] :fields The names of the fields to subscribe to on the items. Required.
|
122
|
-
# @option options [:distinct, :merge] :mode The operation mode of the subscription. Required.
|
139
|
+
# @option options [:command, :distinct, :merge, :raw] :mode The operation mode of the subscription. Required.
|
123
140
|
# @option options [String] :adapter The name of the data adapter from this session's adapter set that should be
|
124
|
-
# used. If `nil` then the default data adapter will be used.
|
141
|
+
# used. If this is not set or is set to `nil` then the default data adapter will be used.
|
125
142
|
# @option options [String] :selector The selector for table items. Optional.
|
126
143
|
# @option options [Float, :unfiltered] :maximum_update_frequency The maximum number of updates the subscription
|
127
144
|
# should receive per second. Defaults to zero which means there is no limit on the update frequency.
|
@@ -140,6 +157,8 @@ module Lightstreamer
|
|
140
157
|
# Stops the specified subscription and removes it from this session. If an error occurs then a {LightstreamerError}
|
141
158
|
# subclass will be raised. To just stop a subscription with the option of restarting it at a later date call
|
142
159
|
# {Subscription#stop} on the subscription itself.
|
160
|
+
#
|
161
|
+
# @param [Subscription] subscription The subscription to stop and remove from this session.
|
143
162
|
def remove_subscription(subscription)
|
144
163
|
subscription.stop
|
145
164
|
|
@@ -147,10 +166,11 @@ module Lightstreamer
|
|
147
166
|
end
|
148
167
|
|
149
168
|
# This method performs a bulk {Subscription#start} on all the passed subscriptions. Calling {Subscription#start} on
|
150
|
-
# each
|
151
|
-
#
|
152
|
-
# a large number of subscriptions. The return value is an array with one entry per
|
153
|
-
# error state returned by the server for that subscription's start request, or `nil`
|
169
|
+
# each subscription individually would also work but requires a separate POST request to be sent for every
|
170
|
+
# subscription, whereas this request starts all of the passed subscriptions in a single POST request which is
|
171
|
+
# significantly faster for a large number of subscriptions. The return value is an array with one entry per
|
172
|
+
# subscription and indicates the error state returned by the server for that subscription's start request, or `nil`
|
173
|
+
# if no error occurred.
|
154
174
|
#
|
155
175
|
# @param [Array<Subscription>] subscriptions The subscriptions to start.
|
156
176
|
#
|
@@ -181,15 +201,16 @@ module Lightstreamer
|
|
181
201
|
# By default the message will be sent synchronously, i.e. the message will be processed by the server and if an
|
182
202
|
# error occurs a {LightstreamerError} subclass will be raised immediately. However, if the `:async` option is true
|
183
203
|
# then the message will be sent asynchronously, and the result of the message send will be reported to all callbacks
|
184
|
-
# that have been registered via {#on_message_result}.
|
204
|
+
# that have been registered via {#on_message_result}. If `:async` is set to `true` then the `:sequence` and
|
205
|
+
# `:number` options must also be specified.
|
185
206
|
#
|
186
207
|
# @param [String] message The message to send to the Lightstreamer server.
|
187
208
|
# @param [Hash] options The options that control messages sent asynchronously.
|
188
209
|
# @option options [Boolean] :async Whether to send the message asynchronously. Defaults to `false`.
|
189
210
|
# @option options [String] :sequence The alphanumeric identifier that identifies a subset of messages that are to
|
190
|
-
# be processed in sequence based on the `:number` given to them. If the special
|
191
|
-
# sequence is used then the associated messages are processed immediately,
|
192
|
-
# with no ordering constraint.
|
211
|
+
# be processed in sequence based on the `:number` given to them. If the special
|
212
|
+
# `"UNORDERED_MESSAGES"` sequence is used then the associated messages are processed immediately,
|
213
|
+
# possibly concurrently, with no ordering constraint.
|
193
214
|
# @option options [Fixnum] :number The progressive number of this message within its sequence. Should start at 1.
|
194
215
|
# @option options [Float] :max_wait The maximum time the server can wait before processing this message if one or
|
195
216
|
# more of the preceding messages in the same sequence have not been received. If not specified then
|
@@ -205,16 +226,17 @@ module Lightstreamer
|
|
205
226
|
PostRequest.execute url, query
|
206
227
|
end
|
207
228
|
|
208
|
-
# Adds the passed block to the list of callbacks that will be run when the outcome of
|
209
|
-
#
|
210
|
-
# The arguments passed to the block are `|sequence, numbers, error|`.
|
229
|
+
# Adds the passed block to the list of callbacks that will be run when the outcome of one or more asynchronous
|
230
|
+
# message sends arrive. The block will be called on a worker thread and so the code that is run by the block must be
|
231
|
+
# thread-safe. The arguments passed to the block are `|sequence, numbers, error|`.
|
211
232
|
#
|
212
233
|
# @param [Proc] callback The callback that is to be run.
|
213
234
|
def on_message_result(&callback)
|
214
235
|
@on_message_result_callbacks << callback
|
215
236
|
end
|
216
237
|
|
217
|
-
# Sends a request to
|
238
|
+
# Sends a request to this session's control connection. If an error occurs then a {LightstreamerError} subclass will
|
239
|
+
# be raised.
|
218
240
|
#
|
219
241
|
# @param [Symbol] operation The control operation to perform.
|
220
242
|
# @param [Hash] options The options to send with the control request.
|
@@ -244,8 +266,7 @@ module Lightstreamer
|
|
244
266
|
end
|
245
267
|
end
|
246
268
|
|
247
|
-
# Processes a single line of incoming stream data
|
248
|
-
# successfully processes it. This method is always run on the processing thread.
|
269
|
+
# Processes a single line of incoming stream data. This method is always run on the processing thread.
|
249
270
|
def process_stream_line(line)
|
250
271
|
return if @subscriptions.any? { |subscription| subscription.process_stream_data line }
|
251
272
|
return if process_send_message_outcome line
|
@@ -253,8 +274,7 @@ module Lightstreamer
|
|
253
274
|
warn "Lightstreamer: unprocessed stream data '#{line}'"
|
254
275
|
end
|
255
276
|
|
256
|
-
# Attempts to process the passed line as a send message outcome message
|
257
|
-
# registered callbacks are run.
|
277
|
+
# Attempts to process the passed line as a send message outcome message.
|
258
278
|
def process_send_message_outcome(line)
|
259
279
|
outcome = SendMessageOutcomeMessage.parse line
|
260
280
|
return unless outcome
|
@@ -66,8 +66,8 @@ module Lightstreamer
|
|
66
66
|
end
|
67
67
|
|
68
68
|
# Reads the next line of streaming data. If the streaming thread is alive then this method blocks the calling thread
|
69
|
-
# until a line of data is available
|
70
|
-
# returned and after that the return value will be `nil`.
|
69
|
+
# until a line of data is available or the streaming thread terminates for any reason. If the streaming thread is
|
70
|
+
# not active then any unconsumed lines will be returned and after that the return value will be `nil`.
|
71
71
|
#
|
72
72
|
# @return [String, nil]
|
73
73
|
def read_line
|
@@ -95,8 +95,8 @@ module Lightstreamer
|
|
95
95
|
end
|
96
96
|
|
97
97
|
def create_new_stream
|
98
|
-
params =
|
99
|
-
|
98
|
+
params = build_params LS_op2: 'create', LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg', LS_user: @session.username,
|
99
|
+
LS_password: @session.password
|
100
100
|
|
101
101
|
params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
|
102
102
|
|
@@ -107,12 +107,23 @@ module Lightstreamer
|
|
107
107
|
end
|
108
108
|
|
109
109
|
def bind_to_existing_stream
|
110
|
-
params =
|
110
|
+
params = build_params LS_session: @session_id
|
111
111
|
|
112
112
|
url = URI.join(control_address, '/lightstreamer/bind_session.txt').to_s
|
113
113
|
execute_stream_post_request url, connect_timeout: 15, query: params
|
114
114
|
end
|
115
115
|
|
116
|
+
def build_params(params)
|
117
|
+
params[:LS_requested_max_bandwidth] = @session.requested_maximum_bandwidth
|
118
|
+
|
119
|
+
if @session.polling_enabled
|
120
|
+
params[:LS_polling] = true
|
121
|
+
params[:LS_polling_millis] = 15_000
|
122
|
+
end
|
123
|
+
|
124
|
+
params
|
125
|
+
end
|
126
|
+
|
116
127
|
def execute_stream_post_request(url, options)
|
117
128
|
@header = StreamConnectionHeader.new
|
118
129
|
|
@@ -130,6 +141,8 @@ module Lightstreamer
|
|
130
141
|
end
|
131
142
|
|
132
143
|
def process_stream_line(line)
|
144
|
+
return if line =~ /^(PROBE|Preamble:.*)$/
|
145
|
+
|
133
146
|
if @header
|
134
147
|
process_header_line line
|
135
148
|
else
|
@@ -151,18 +164,17 @@ module Lightstreamer
|
|
151
164
|
@error = @header.error
|
152
165
|
|
153
166
|
return if header_incomplete
|
167
|
+
@header = nil
|
154
168
|
|
155
169
|
signal_connect_result_ready
|
156
|
-
|
157
|
-
@header = nil
|
158
170
|
end
|
159
171
|
|
160
172
|
def process_body_line(line)
|
161
|
-
if line =~ /^LOOP
|
173
|
+
if line =~ /^LOOP( \d+|)$/
|
162
174
|
@loop = true
|
163
|
-
elsif line =~ /^END/
|
175
|
+
elsif line =~ /^END( \d+|)/
|
164
176
|
@error = Errors::SessionEndError.new line[4..-1]
|
165
|
-
elsif line
|
177
|
+
elsif !line.empty?
|
166
178
|
@queue.push line
|
167
179
|
end
|
168
180
|
end
|
@@ -19,16 +19,17 @@ module Lightstreamer
|
|
19
19
|
# @return [Array]
|
20
20
|
attr_reader :fields
|
21
21
|
|
22
|
-
# The operation mode of this subscription.
|
22
|
+
# The operation mode of this subscription. The four supported operation modes are: `:command`, `:distinct`, `:merge`
|
23
|
+
# and `:raw`. See the Lightstreamer documentation for details on the different modes.
|
23
24
|
#
|
24
|
-
# @return [:distinct, :merge]
|
25
|
+
# @return [:command, :distinct, :merge, :raw]
|
25
26
|
attr_reader :mode
|
26
27
|
|
27
|
-
# The name of the data adapter from the Lightstreamer session's adapter set
|
28
|
-
#
|
28
|
+
# The name of the data adapter from the Lightstreamer session's adapter set to use, or `nil` to use the default
|
29
|
+
# data adapter.
|
29
30
|
#
|
30
31
|
# @return [String, nil]
|
31
|
-
attr_reader :
|
32
|
+
attr_reader :data_adapter
|
32
33
|
|
33
34
|
# The selector for table items.
|
34
35
|
#
|
@@ -37,7 +38,8 @@ module Lightstreamer
|
|
37
38
|
|
38
39
|
# The maximum number of updates this subscription should receive per second. If this is set to zero, which is the
|
39
40
|
# 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
|
+
# used for this subscription and it is possible for overflows to occur (see {#on_overflow}). If {#mode} is `:raw`
|
42
|
+
# then the maximum update frequency is treated as `:unfiltered` regardless of its actual value.
|
41
43
|
#
|
42
44
|
# @return [Float, :unfiltered]
|
43
45
|
attr_reader :maximum_update_frequency
|
@@ -59,7 +61,7 @@ module Lightstreamer
|
|
59
61
|
@items = options.fetch(:items)
|
60
62
|
@fields = options.fetch(:fields)
|
61
63
|
@mode = options.fetch(:mode).to_sym
|
62
|
-
@
|
64
|
+
@data_adapter = options[:data_adapter]
|
63
65
|
@selector = options[:selector]
|
64
66
|
@maximum_update_frequency = sanitize_frequency options[:maximum_update_frequency]
|
65
67
|
|
@@ -86,18 +88,21 @@ module Lightstreamer
|
|
86
88
|
# subscription is initiated on the server and begins buffering incoming data, however this data will
|
87
89
|
# not be sent to the client for processing until {#unsilence} is called.
|
88
90
|
# @option options [Boolean, Fixnum] :snapshot Controls whether the server should send a snapshot of this
|
89
|
-
# subscription's items.
|
90
|
-
#
|
91
|
-
# subscription's {#mode} is `:distinct` then `:snapshot` can also be an integer
|
92
|
-
# number of events the server should send as part of the snapshot. If this latter
|
93
|
-
#
|
94
|
-
# item is complete.
|
91
|
+
# subscription's items. The default value is `false` which means then the server will not send
|
92
|
+
# snapshot information. If set to `true` then the server will send snapshot information if it is
|
93
|
+
# available. If this subscription's {#mode} is `:distinct` then `:snapshot` can also be an integer
|
94
|
+
# specifying the number of events the server should send as part of the snapshot. If this latter
|
95
|
+
# option is used, or {#mode} is `:command`, then any callbacks registered with {#on_end_of_snapshot}
|
96
|
+
# will be called once the snapshot for each item is complete. This option is ignored when {#mode} is
|
97
|
+
# `:raw`.
|
95
98
|
def start(options = {})
|
96
|
-
|
99
|
+
return if @active
|
100
|
+
|
101
|
+
session.control_request(*start_control_request_args(options))
|
97
102
|
@active = true
|
98
103
|
end
|
99
104
|
|
100
|
-
# Returns the arguments to pass to to {Session#control_request} in order
|
105
|
+
# Returns the arguments to pass to to {Session#control_request} in order to start this subscription with the given
|
101
106
|
# options.
|
102
107
|
#
|
103
108
|
# @param [Hash] options The options to start the subscription with.
|
@@ -106,15 +111,15 @@ module Lightstreamer
|
|
106
111
|
def start_control_request_args(options = {})
|
107
112
|
operation = options[:silent] ? :add_silent : :add
|
108
113
|
|
109
|
-
options = { LS_table: id, LS_mode: mode.to_s.upcase, LS_id: items, LS_schema: fields,
|
110
|
-
|
114
|
+
options = { LS_table: id, LS_mode: mode.to_s.upcase, LS_id: items, LS_schema: fields, LS_selector: selector,
|
115
|
+
LS_data_adapter: data_adapter, LS_requested_max_frequency: maximum_update_frequency,
|
111
116
|
LS_snapshot: options.fetch(:snapshot, false) }
|
112
117
|
|
113
118
|
[operation, options]
|
114
119
|
end
|
115
120
|
|
116
|
-
# Unsilences this subscription if it was initially started in silent mode
|
117
|
-
#
|
121
|
+
# Unsilences this subscription if it was initially started in silent mode by passing `silent: true` to {#start}. If
|
122
|
+
# this subscription was not started in silent mode then this method has no effect. If an error occurs then a
|
118
123
|
# {LightstreamerError} subclass will be raised.
|
119
124
|
def unsilence
|
120
125
|
session.control_request :start, LS_table: id
|
@@ -129,7 +134,8 @@ module Lightstreamer
|
|
129
134
|
|
130
135
|
# Sets this subscription's maximum update frequency. This can be done while a subscription is streaming data in
|
131
136
|
# order to change its update frequency limit, but an actively streaming subscription cannot switch between filtered
|
132
|
-
# and unfiltered dispatching, and {TableModificationNotAllowedError} will be raised if this is attempted.
|
137
|
+
# and unfiltered dispatching, and {TableModificationNotAllowedError} will be raised if this is attempted. If {#mode}
|
138
|
+
# is `:raw` then the maximum update frequency is treated as `:unfiltered` regardless of its actual value.
|
133
139
|
#
|
134
140
|
# @param [Float, :unfiltered] new_frequency The new maximum update frequency. See {#maximum_update_frequency} for
|
135
141
|
# details.
|
@@ -142,13 +148,42 @@ module Lightstreamer
|
|
142
148
|
# Clears all current data stored for this subscription. New data will continue to be processed as it becomes
|
143
149
|
# available.
|
144
150
|
def clear_data
|
145
|
-
@data = (0...items.size).map {
|
151
|
+
@data_mutex.synchronize { @data = (0...items.size).map { SubscriptionItemData.new } }
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns a copy of the current data of one of this subscription's items. If {#mode} is `:merge` then the returned
|
155
|
+
# object will be a hash of the item's state, if it is `:command` then an array of row data for the item will be
|
156
|
+
# returned, and if it is `:distinct` or `:raw` then just the most recent update received for the item will be
|
157
|
+
# returned. The return value will be `nil` if no data for the item has been set or been received.
|
158
|
+
#
|
159
|
+
# @param [String] item_name The name of the item to return the current data for.
|
160
|
+
#
|
161
|
+
# @return [Hash, Array, nil] A copy of the item data.
|
162
|
+
def item_data(item_name)
|
163
|
+
index = @items.index item_name
|
164
|
+
raise ArgumentError, 'Unknown item' unless index
|
165
|
+
|
166
|
+
@data_mutex.synchronize { @data[index].data && @data[index].data.dup }
|
167
|
+
end
|
168
|
+
|
169
|
+
# Sets the current data for the item with the specified name. This is only allowed when {mode} is `:command` or
|
170
|
+
# `:merge`. Raises an exception if the specified item name or item data is invalid.
|
171
|
+
#
|
172
|
+
# @param [String] item_name The name of the item to set the data for.
|
173
|
+
# @param [Hash, Array<Hash>] item_data The new data for the item. If {#mode} is `:merge` this must be a hash. If
|
174
|
+
# {#mode} is `:command` then this must be an `Array<Hash>` and each hash entry must have a unique `:key`
|
175
|
+
# value.
|
176
|
+
def set_item_data(item_name, item_data)
|
177
|
+
index = @items.index item_name
|
178
|
+
raise ArgumentError, 'Unknown item' unless index
|
179
|
+
|
180
|
+
@data_mutex.synchronize { @data[index].set_data item_data, mode }
|
146
181
|
end
|
147
182
|
|
148
183
|
# Adds the passed block to the list of callbacks that will be run when new data for this subscription arrives. The
|
149
184
|
# block will be called on a worker thread and so the code that is run by the block must be thread-safe. The
|
150
|
-
# arguments passed to the block are `|subscription, item_name, item_data,
|
151
|
-
#
|
185
|
+
# arguments passed to the block are `|subscription, item_name, item_data, new_data|`. If {#mode} is `:distinct`
|
186
|
+
# or `:raw` then `item_data` and `new_data` will be the same.
|
152
187
|
#
|
153
188
|
# @param [Proc] callback The callback that is to be run when new data arrives.
|
154
189
|
def on_data(&callback)
|
@@ -156,8 +191,10 @@ module Lightstreamer
|
|
156
191
|
end
|
157
192
|
|
158
193
|
# Adds the passed block to the list of callbacks that will be run when the server reports an overflow for this
|
159
|
-
# subscription.
|
160
|
-
#
|
194
|
+
# subscription. This is only relevant when this subscription's {#mode} is `:command` or `:raw`, or if
|
195
|
+
# {#maximum_update_frequency} is `:unfiltered`. The block will be called on a worker thread and so the code that is
|
196
|
+
# run by the block must be thread-safe. The arguments passed to the block are `|subscription, item_name,
|
197
|
+
# overflow_size|`.
|
161
198
|
#
|
162
199
|
# @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
|
163
200
|
def on_overflow(&callback)
|
@@ -165,51 +202,27 @@ module Lightstreamer
|
|
165
202
|
end
|
166
203
|
|
167
204
|
# Adds the passed block to the list of callbacks that will be run when the server reports an end-of-snapshot
|
168
|
-
# notification for this subscription.
|
169
|
-
#
|
205
|
+
# notification for this subscription. End-of-snapshot notifications are only sent when {#mode} is `:command` or
|
206
|
+
# `:distinct` and `snapshot: true` was passed to {#start}. The block will be called on a worker thread and so the
|
207
|
+
# code that is run by the block must be thread-safe. The arguments passed to the block are `|subscription,
|
208
|
+
# item_name|`.
|
170
209
|
#
|
171
210
|
# @param [Proc] callback The callback that is to be run when an overflow is reported for this subscription.
|
172
211
|
def on_end_of_snapshot(&callback)
|
173
212
|
@data_mutex.synchronize { @callbacks[:on_end_of_snapshot] << callback }
|
174
213
|
end
|
175
214
|
|
176
|
-
# Removes all {#on_data} and {#
|
215
|
+
# Removes all {#on_data}, {#on_overflow} and {#on_end_of_snapshot} callbacks present on this subscription.
|
177
216
|
def clear_callbacks
|
178
217
|
@data_mutex.synchronize { @callbacks = { on_data: [], on_overflow: [], on_end_of_snapshot: [] } }
|
179
218
|
end
|
180
219
|
|
181
|
-
# Returns a copy of the current data of one of this subscription's items.
|
182
|
-
#
|
183
|
-
# @param [String] item_name The name of the item to return the current data for.
|
184
|
-
#
|
185
|
-
# @return [Hash] A copy of the item data.
|
186
|
-
def item_data(item_name)
|
187
|
-
index = @items.index item_name
|
188
|
-
raise ArgumentError, 'Unknown item' unless index
|
189
|
-
|
190
|
-
@data_mutex.synchronize { @data[index].dup }
|
191
|
-
end
|
192
|
-
|
193
|
-
# Sets the current data for the item with the specified name.
|
194
|
-
#
|
195
|
-
# @param [String] item_name The name of the item to set the data for.
|
196
|
-
# @param [Hash] item_data The new data for the item.
|
197
|
-
def set_item_data(item_name, item_data)
|
198
|
-
index = @items.index item_name
|
199
|
-
raise ArgumentError, 'Unknown item' unless index
|
200
|
-
|
201
|
-
raise ArgumentError, 'Item data must be a hash' unless item_data.is_a? Hash
|
202
|
-
|
203
|
-
@data_mutex.synchronize { @data[index] = item_data.dup }
|
204
|
-
end
|
205
|
-
|
206
220
|
# Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
|
207
221
|
# to be called by the session's processing thread.
|
208
222
|
#
|
209
223
|
# @param [String] line The line of stream data to process.
|
210
224
|
#
|
211
|
-
# @return [Boolean] Whether the passed line of stream data was
|
212
|
-
# processed by it.
|
225
|
+
# @return [Boolean] Whether the passed line of stream data was processed by this subscription.
|
213
226
|
#
|
214
227
|
# @private
|
215
228
|
def process_stream_data(line)
|
@@ -229,16 +242,10 @@ module Lightstreamer
|
|
229
242
|
def process_update_message(message)
|
230
243
|
return unless message
|
231
244
|
|
232
|
-
@data_mutex.synchronize
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
data = @data[item_index]
|
237
|
-
|
238
|
-
data.replace(new_values) if mode == :distinct
|
239
|
-
data.merge!(new_values) if mode == :merge
|
240
|
-
|
241
|
-
run_callbacks :on_data, @items[item_index], data, new_values
|
245
|
+
@data_mutex.synchronize do
|
246
|
+
@data[message.item_index].send "process_new_#{mode}_data", message.data.dup
|
247
|
+
run_callbacks :on_data, @items[message.item_index], @data[message.item_index].data, message.data
|
248
|
+
end
|
242
249
|
end
|
243
250
|
|
244
251
|
def process_overflow_message(message)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class used by {Subscription} to process incoming item data according to the four different subscription
|
3
|
+
# modes.
|
4
|
+
#
|
5
|
+
# @private
|
6
|
+
class SubscriptionItemData
|
7
|
+
# The current item data. Item data is a hash for all subscription modes except `:command` when it is an array.
|
8
|
+
#
|
9
|
+
# @return [Hash, Array, nil]
|
10
|
+
attr_accessor :data
|
11
|
+
|
12
|
+
# Explicitly sets this item data. See {Subscription#set_item_data} for details.
|
13
|
+
#
|
14
|
+
# @param [Array, Hash] new_data The new data for the item.
|
15
|
+
# @param [:command, :merge] mode The subscription mode.
|
16
|
+
def set_data(new_data, mode)
|
17
|
+
raise ArgumentError, "Data can't be set unless mode is :command or :merge" unless [:command, :merge].include? mode
|
18
|
+
raise ArgumentError, 'Data must be a hash when in merge mode' if mode == :merge && !new_data.is_a?(Hash)
|
19
|
+
|
20
|
+
validate_rows new_data if mode == :command
|
21
|
+
|
22
|
+
@data = new_data.dup
|
23
|
+
end
|
24
|
+
|
25
|
+
# Processes new data for the `:command` subscription mode.
|
26
|
+
#
|
27
|
+
# @param [Hash] new_data The new data.
|
28
|
+
def process_new_command_data(new_data)
|
29
|
+
@data ||= []
|
30
|
+
|
31
|
+
key = row_key new_data
|
32
|
+
command = new_data.delete(:command) || new_data.delete('command')
|
33
|
+
|
34
|
+
send "process_#{command.to_s.downcase}_command", key, new_data
|
35
|
+
end
|
36
|
+
|
37
|
+
# Processes new data for the `:distinct` subscription mode.
|
38
|
+
#
|
39
|
+
# @param [Hash] new_data The new data.
|
40
|
+
def process_new_distinct_data(new_data)
|
41
|
+
@data = new_data
|
42
|
+
end
|
43
|
+
|
44
|
+
# Processes new data for the `:merge` subscription mode.
|
45
|
+
#
|
46
|
+
# @param [Hash] new_data The new data.
|
47
|
+
def process_new_merge_data(new_data)
|
48
|
+
@data ||= {}
|
49
|
+
@data.merge! new_data
|
50
|
+
end
|
51
|
+
|
52
|
+
# Processes new data for the `:raw` subscription mode.
|
53
|
+
#
|
54
|
+
# @param [Hash] new_data The new data.
|
55
|
+
def process_new_raw_data(new_data)
|
56
|
+
@data = new_data
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def validate_rows(rows)
|
62
|
+
raise ArgumentError, 'Data must be an array when in command mode' unless rows.is_a? Array
|
63
|
+
|
64
|
+
keys = rows.map { |row| row_key row }
|
65
|
+
raise ArgumentError, 'Each row must have a unique key' if keys.uniq.size != rows.size
|
66
|
+
end
|
67
|
+
|
68
|
+
def row_key(row)
|
69
|
+
return row[:key] if row.key? :key
|
70
|
+
return row['key'] if row.key? 'key'
|
71
|
+
|
72
|
+
raise ArgumentError, 'Row does not have a key'
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_add_command(key, new_data)
|
76
|
+
process_update_command key, new_data
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_update_command(key, new_data)
|
80
|
+
row_to_update = @data.detect { |row| row_key(row) == key }
|
81
|
+
|
82
|
+
if row_to_update
|
83
|
+
row_to_update.merge! new_data
|
84
|
+
else
|
85
|
+
data << new_data
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_delete_command(key, _new_data)
|
90
|
+
@data.delete_if { |row| row_key(row) == key }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lightstreamer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.8'
|
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-
|
11
|
+
date: 2016-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: excon
|
@@ -189,6 +189,7 @@ files:
|
|
189
189
|
- lib/lightstreamer/stream_connection.rb
|
190
190
|
- lib/lightstreamer/stream_connection_header.rb
|
191
191
|
- lib/lightstreamer/subscription.rb
|
192
|
+
- lib/lightstreamer/subscription_item_data.rb
|
192
193
|
- lib/lightstreamer/version.rb
|
193
194
|
homepage: https://github.com/rviney/lightstreamer
|
194
195
|
licenses:
|