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 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: