lightstreamer 0.7 → 0.8

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: 67492665643def7bc43ea5ab072416c91d7e494d
4
- data.tar.gz: 3f47646a1ff2d3c939c65588304e57234f893490
3
+ metadata.gz: 2fbedc6ddd408cf66740c82acb37a83fb1954b13
4
+ data.tar.gz: e259733aa9ab2bd100a1cee9a58ea120de44614c
5
5
  SHA512:
6
- metadata.gz: 7510f00c805e9f83758e91b82c8682f22472d9c367a5a348faa4e46c8422e479c530bd96210c5eced1d7f56537ad39df14b6cf30dbf9d1a4787e1ea16695e6e5
7
- data.tar.gz: 184073f14a2e2d34b9f208201526d6357a991d2a4bb113f40da21f80b112a0487f1177c12a24bd715a0e0c5b81d59997d9b6d64b603cff9c7ef89069e0455e02
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. Written against the
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 setup a Lightstreamer session, a subscription, then print streaming
34
- output as it comes in.
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, adapter: 'QUOTE_ADAPTER'
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, new_values|
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 --adapter QUOTE_ADAPTER \
79
- --items item1 item2 item3 item4 item5 --fields ask bid stock_name time
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 a full list of available options for the command-line client run the following command:
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
- option :adapter, desc: 'The name of the data adapter to stream data from'
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), default: :merge, desc: 'The operation mode'
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], adapter: options[:adapter],
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, new_values)
65
- @queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
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)
@@ -161,7 +161,7 @@ module Lightstreamer
161
161
 
162
162
  # Initializes this session end error with the specified cause code.
163
163
  #
164
- # @param [Session?] cause_code See {#cause_code} for details.
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 entry if
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 values specified by this update message.
11
+ # The field data specified by this update message.
12
12
  #
13
13
  # @return [Array]
14
- attr_accessor :values
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.values = parse_values match.captures[1..-1], fields
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 parse_values(values, fields)
39
+ def parse_field_values(field_values, fields)
40
40
  hash = {}
41
41
 
42
- values.each_with_index do |value, index|
43
- next if value == ''
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 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. If an error occurs then a {LightstreamerError} subclass will be raised.
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 of them individually would also work but requires a separate POST request to be sent for every subscription.
151
- # This request starts all of the passed subscriptions in a single POST request which is significantly faster for
152
- # a large number of subscriptions. The return value is an array with one entry per subscription and indicates the
153
- # error state returned by the server for that subscription's start request, or `nil` if no error occurred.
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 `UNORDERED_MESSAGES`
191
- # sequence is used then the associated messages are processed immediately, possibly concurrently,
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 an asynchronous message send
209
- # arrives. The block will be called on a worker thread and so the code that is run by the block must be thread-safe.
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 the control connection. If an error occurs then a {LightstreamerError} subclass will be raised.
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 by passing it to all the subscriptions until one
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, and if is such a message then the
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. If the streaming thread is not active then any unconsumed lines will be
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 = { LS_op2: 'create', LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg', LS_user: @session.username,
99
- LS_password: @session.password, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
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 = { LS_session: @session_id, LS_requested_max_bandwidth: @session.requested_maximum_bandwidth }
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 !~ /^(PROBE|Preamble:.*)$/
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 that should be used, or `nil` to use the
28
- # default data adapter.
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 :adapter
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
- @adapter = options[:adapter]
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. If `false` then the server does not send snapshot information (this is the
90
- # default). If `true` then the server will send snapshot information if it's available. If this
91
- # subscription's {#mode} is `:distinct` then `:snapshot` can also be an integer specifying the
92
- # number of events the server should send as part of the snapshot. If this latter option is used
93
- # then any callbacks registered with {#on_end_of_snapshot} will be called once the snapshot for each
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
- session.control_request(*start_control_request_args(options)) unless @active
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 ot start this subscription with the given
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, LS_data_adapter: adapter,
110
- LS_requested_max_frequency: maximum_update_frequency, LS_selector: selector,
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 (by passing `silent: true` to {#start}).
117
- # If this subscription was not started in silent mode then this method has no effect. If an error occurs then a
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, new_values|`. If {#mode} is `:distinct`
151
- # then the values of `item_data` and `new_values` will be the same.
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. The block will be called on a worker thread and so the code that is run by the block must be
160
- # thread-safe. The arguments passed to the block are `|subscription, item_name, overflow_size|`.
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. The block will be called on a worker thread and so the code that is run by the
169
- # block must be thread-safe. The arguments passed to the block are `|subscription, item_name|`.
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 {#on_overflow} callbacks present on this subscription.
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 relevant to this subscription and was successfully
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 { process_new_values message.item_index, message.values }
233
- end
234
-
235
- def process_new_values(item_index, new_values)
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
@@ -1,4 +1,4 @@
1
1
  module Lightstreamer
2
2
  # The version of this gem.
3
- VERSION = '0.7'.freeze
3
+ VERSION = '0.8'.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.7'
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-07-31 00:00:00.000000000 Z
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: