lightstreamer 0.7 → 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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:
|