lightstreamer 0.1
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +25 -0
- data/README.md +109 -0
- data/bin/lightstreamer +7 -0
- data/lib/lightstreamer.rb +18 -0
- data/lib/lightstreamer/cli/main.rb +24 -0
- data/lib/lightstreamer/cli/stream_command.rb +50 -0
- data/lib/lightstreamer/control_connection.rb +70 -0
- data/lib/lightstreamer/line_buffer.rb +22 -0
- data/lib/lightstreamer/protocol_error.rb +22 -0
- data/lib/lightstreamer/request_error.rb +21 -0
- data/lib/lightstreamer/session.rb +143 -0
- data/lib/lightstreamer/stream_connection.rb +79 -0
- data/lib/lightstreamer/subscription.rb +194 -0
- data/lib/lightstreamer/version.rb +4 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bf90908d8a97af1417f8391f16e2c26a10baf73d
|
4
|
+
data.tar.gz: 6e89354b80f3712ca9c4278f09342123d3844878
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a17d1e9276c986f6c436e3aad883f4f068b319e9e88e30b7e7eb844b1af33ca18c35dab74a76e2e25763263199c7f251acf45d999a07afeb749b424d6ea58985
|
7
|
+
data.tar.gz: d359e20a661d5dc953f5ba02d3984f4db0206886b38ebdc1ab555c20c5b367eab9aa8499071eef6ba6f074da116925cf2774742c043e314bdd21f419126535a9
|
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Copyright © 2015-2016 Richard Viney
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person
|
7
|
+
obtaining a copy of this software and associated documentation
|
8
|
+
files (the “Software”), to deal in the Software without
|
9
|
+
restriction, including without limitation the rights to use,
|
10
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the
|
12
|
+
Software is furnished to do so, subject to the following
|
13
|
+
conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be
|
16
|
+
included in all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
22
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
24
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
25
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Ruby Lightstreamer Client Gem
|
2
|
+
|
3
|
+
[![Gem][gem-badge]][gem-link]
|
4
|
+
[![Build Status][travis-ci-badge]][travis-ci-link]
|
5
|
+
[![Test Coverage][test-coverage-badge]][test-coverage-link]
|
6
|
+
[![Code Climate][code-climate-badge]][code-climate-link]
|
7
|
+
[![Dependencies][dependencies-badge]][dependencies-link]
|
8
|
+
[![Documentation][documentation-badge]][documentation-link]
|
9
|
+
[![License][license-badge]][license-link]
|
10
|
+
|
11
|
+
Easily interface with a Lightstreamer service from Ruby. Written against the
|
12
|
+
[official API specification](http://www.lightstreamer.com/docs/client_generic_base/Network%20Protocol%20Tutorial.pdf).
|
13
|
+
|
14
|
+
## License
|
15
|
+
|
16
|
+
Licensed under the MIT license. You must read and agree to its terms to use this software.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Install the latest version of the `lightstreamer` gem with the following command:
|
21
|
+
|
22
|
+
```
|
23
|
+
$ gem install lightstreamer
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage — Library
|
27
|
+
|
28
|
+
The two primary classes that make up the public API are:
|
29
|
+
|
30
|
+
- [`Lightstreamer::Session`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Session)
|
31
|
+
- [`Lightstreamer::Subscription`](http://www.rubydoc.info/github/rviney/lightstreamer/Lightstreamer/Subscription)
|
32
|
+
|
33
|
+
The following code snippet demonstrates how to setup a Lightstreamer session, a subscription, then print streaming
|
34
|
+
output as it comes in.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'lightstreamer'
|
38
|
+
|
39
|
+
# Create a new session that connects to the Lightstreamer demo server, which needs no authentication
|
40
|
+
session = Lightstreamer::Session.new server_url: 'http://push.lightstreamer.com',
|
41
|
+
adapter_set: 'DEMO', username: '', password: ''
|
42
|
+
|
43
|
+
# Connect the session
|
44
|
+
session.connect
|
45
|
+
|
46
|
+
# Create a new subscription that subscribes to five items and to four fields on each item
|
47
|
+
subscription = Lightstreamer::Subscription.new items: %w(item1 item2 item3 item4 item5),
|
48
|
+
fields: [:time, :stock_name, :bid, :ask],
|
49
|
+
mode: :merge, adapter: 'QUOTE_ADAPTER'
|
50
|
+
|
51
|
+
# Create a thread-safe queue
|
52
|
+
queue = Queue.new
|
53
|
+
|
54
|
+
# When new data becomes available for the subscription it will be put on the queue. This callback
|
55
|
+
# will be run on a worker thread.
|
56
|
+
subscription.add_data_callback do |subscription, item_name, item_data, new_values|
|
57
|
+
queue.push item_data
|
58
|
+
end
|
59
|
+
|
60
|
+
# Activate the subscription
|
61
|
+
session.subscribe subscription
|
62
|
+
|
63
|
+
# Loop printing out new data as soon as it becomes available on the queue
|
64
|
+
loop do
|
65
|
+
data = queue.pop
|
66
|
+
puts "#{data[:time]} - #{data[:stock_name]} - bid: #{data[:bid]}, ask: #{data[:ask]}"
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Usage — Command-Line Client
|
71
|
+
|
72
|
+
This gem provides a simple command-line client that can connect to a Lightstreamer server, activate a
|
73
|
+
subscription, then print streaming output from the server as it becomes available.
|
74
|
+
|
75
|
+
To print streaming data from the demo server run the following command:
|
76
|
+
|
77
|
+
```
|
78
|
+
lightstreamer --address http://push.lightstreamer.com --adapter-set DEMO --adapter QUOTE_ADAPTER \
|
79
|
+
--items item1 item2 item3 item4 item5 --fields time stock_name bid ask bid
|
80
|
+
```
|
81
|
+
|
82
|
+
To see a full list of available options run the following command:
|
83
|
+
|
84
|
+
```
|
85
|
+
lightstreamer help stream
|
86
|
+
```
|
87
|
+
|
88
|
+
## Documentation
|
89
|
+
|
90
|
+
API documentation is available [here](http://www.rubydoc.info/github/rviney/lightstreamer).
|
91
|
+
|
92
|
+
## Contributors
|
93
|
+
|
94
|
+
Gem created by Richard Viney. All contributions welcome.
|
95
|
+
|
96
|
+
[gem-link]: https://rubygems.org/gems/lightstreamer
|
97
|
+
[gem-badge]: https://badge.fury.io/rb/lightstreamer.svg
|
98
|
+
[travis-ci-link]: http://travis-ci.org/rviney/lightstreamer
|
99
|
+
[travis-ci-badge]: https://travis-ci.org/rviney/lightstreamer.svg?branch=master
|
100
|
+
[test-coverage-link]: https://codeclimate.com/github/rviney/lightstreamer/coverage
|
101
|
+
[test-coverage-badge]: https://codeclimate.com/github/rviney/lightstreamer/badges/coverage.svg
|
102
|
+
[code-climate-link]: https://codeclimate.com/github/rviney/lightstreamer
|
103
|
+
[code-climate-badge]: https://codeclimate.com/github/rviney/lightstreamer/badges/gpa.svg
|
104
|
+
[dependencies-link]: https://gemnasium.com/rviney/lightstreamer
|
105
|
+
[dependencies-badge]: https://gemnasium.com/rviney/lightstreamer.svg
|
106
|
+
[documentation-link]: https://inch-ci.org/github/rviney/lightstreamer
|
107
|
+
[documentation-badge]: https://inch-ci.org/github/rviney/lightstreamer.svg?branch=master
|
108
|
+
[license-link]: https://github.com/rviney/lightstreamer/blob/master/LICENSE.md
|
109
|
+
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
data/bin/lightstreamer
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
require 'lightstreamer/control_connection'
|
5
|
+
require 'lightstreamer/line_buffer'
|
6
|
+
require 'lightstreamer/protocol_error'
|
7
|
+
require 'lightstreamer/request_error'
|
8
|
+
require 'lightstreamer/session'
|
9
|
+
require 'lightstreamer/stream_connection'
|
10
|
+
require 'lightstreamer/subscription'
|
11
|
+
require 'lightstreamer/version'
|
12
|
+
|
13
|
+
require 'lightstreamer/cli/main'
|
14
|
+
require 'lightstreamer/cli/stream_command'
|
15
|
+
|
16
|
+
# This module contains all the code for the Lightstreamer gem. See `README.md` to get started with using this gem.
|
17
|
+
module Lightstreamer
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This module contains the code for the CLI frontend. See `README.md` for usage details.
|
3
|
+
module CLI
|
4
|
+
# Implements the `lightstreamer` command-line client.
|
5
|
+
class Main < Thor
|
6
|
+
default_task :stream
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# This is the initial entry point for the execution of the command-line client. It is responsible for the
|
10
|
+
# --version/-v options and then invoking the main application.
|
11
|
+
#
|
12
|
+
# @param [Array<String>] argv The array of command-line arguments.
|
13
|
+
def bootstrap(argv)
|
14
|
+
if argv.index('--version') || argv.index('-v')
|
15
|
+
puts VERSION
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
|
19
|
+
start argv
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
module CLI
|
3
|
+
# Implements the `lightstreamer stream` command.
|
4
|
+
class Main < Thor
|
5
|
+
desc 'stream', 'Streams a set of items and fields from a Lightstreamer server and prints the live output'
|
6
|
+
|
7
|
+
option :address, required: true, desc: 'The address of the Lightstreamer server'
|
8
|
+
option :username, desc: 'The username for the session'
|
9
|
+
option :password, desc: 'The password for the session'
|
10
|
+
option :adapter_set, desc: 'The name of the adapter set for the session'
|
11
|
+
option :adapter, desc: 'The name of the data adapter to stream data from'
|
12
|
+
option :items, type: :array, required: true, desc: 'The names of the item(s) to stream'
|
13
|
+
option :fields, type: :array, required: true, desc: 'The field(s) to stream'
|
14
|
+
option :mode, enum: %w(distinct merge), default: :merge, desc: 'The operation mode'
|
15
|
+
|
16
|
+
def stream
|
17
|
+
session = create_session
|
18
|
+
session.connect
|
19
|
+
|
20
|
+
@queue = Queue.new
|
21
|
+
|
22
|
+
session.subscribe create_subscription
|
23
|
+
|
24
|
+
loop do
|
25
|
+
puts @queue.pop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Creates a new session from the specified options.
|
32
|
+
def create_session
|
33
|
+
Lightstreamer::Session.new server_url: options[:address], username: options[:username],
|
34
|
+
password: options[:password], adapter_set: options[:adapter_set]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates a new subscription from the specified options.
|
38
|
+
def create_subscription
|
39
|
+
subscription = Lightstreamer::Subscription.new items: options[:items], fields: options[:fields],
|
40
|
+
mode: options[:mode], adapter: options[:adapter]
|
41
|
+
|
42
|
+
subscription.add_data_callback do |_subscription, item_name, _item_data, new_values|
|
43
|
+
@queue.push "#{item_name} - #{new_values.map { |key, value| "#{key}: #{value}" }.join ', '}"
|
44
|
+
end
|
45
|
+
|
46
|
+
subscription
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This is an internal class used by {Session} and is responsible for sending Lightstreamer control requests.
|
3
|
+
class ControlConnection
|
4
|
+
# Initializes this class for sending Lightstreamer control requests using the specified session ID and control
|
5
|
+
# address.
|
6
|
+
#
|
7
|
+
# @param [String] session_id The Lightstreamer session ID.
|
8
|
+
# @param [String] control_url The URL of the server to send Lightstreamer control requests to.
|
9
|
+
def initialize(session_id, control_url)
|
10
|
+
@session_id = session_id
|
11
|
+
@control_url = URI.join(control_url, '/lightstreamer/control.txt').to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sends a Lightstreamer control request with the specified options. If an error occurs then {RequestError} or
|
15
|
+
# {ProtocolError} will be raised.
|
16
|
+
#
|
17
|
+
# @param [Hash] options The control request options.
|
18
|
+
# @option options [Fixnum] :table The ID of the table this request pertains to. Required.
|
19
|
+
# @option options [:add, :add_silent, :start, :delete] :operation The operation to perform. Required.
|
20
|
+
# @option options [String] :adapter The name of the data adapter to use. Optional.
|
21
|
+
# @option options [Array<String>] :items The names of the items that this request pertains to. Required if
|
22
|
+
# `:operation` is `:add` or `:add_silent`.
|
23
|
+
# @option options [Array<String>] :fields The names of the fields that this request pertains to. Required if
|
24
|
+
# `:operation` is `:add` or `:add_silent`.
|
25
|
+
# @option options [:raw, :merge, :distinct, :command] :mode The subscription mode.
|
26
|
+
def execute(options)
|
27
|
+
result = execute_post_request build_payload(options)
|
28
|
+
|
29
|
+
return if result.first == 'OK'
|
30
|
+
raise ProtocolError, result[2], result[1] if result.first == 'ERROR'
|
31
|
+
|
32
|
+
warn "Lightstreamer: unexpected response from server: #{result}"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Executes a POST request to the control address with the specified payload. Raises an error if the HTTP request
|
38
|
+
# fails. Returns the response from the server split into individual lines.
|
39
|
+
def execute_post_request(payload)
|
40
|
+
response = RestClient::Request.execute method: :post, url: @control_url, payload: payload
|
41
|
+
|
42
|
+
response.body.split("\n").map(&:strip)
|
43
|
+
rescue RestClient::Exception => exception
|
44
|
+
raise RequestError, exception.message
|
45
|
+
rescue SocketError => socket_error
|
46
|
+
raise RequestError, socket_error
|
47
|
+
end
|
48
|
+
|
49
|
+
# Constructs the payload for a Lightstreamer control request based on the given options hash. See {#execute} for
|
50
|
+
# details on the supported keys.
|
51
|
+
def build_payload(options)
|
52
|
+
params = {
|
53
|
+
LS_session: @session_id,
|
54
|
+
LS_table: options.fetch(:table),
|
55
|
+
LS_op: options.fetch(:operation)
|
56
|
+
}
|
57
|
+
|
58
|
+
build_optional_payload_fields options, params
|
59
|
+
|
60
|
+
URI.encode_www_form params
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_optional_payload_fields(options, params)
|
64
|
+
params[:LS_data_adapter] = options[:adapter] if options.key? :adapter
|
65
|
+
params[:LS_id] = options[:items].join(' ') if options.key? :items
|
66
|
+
params[:LS_schema] = options[:fields].map(&:to_s).join(' ') if options.key? :fields
|
67
|
+
params[:LS_mode] = options[:mode].to_s.upcase if options.key? :mode
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Helper class that takes an incoming stream of ASCII data and yields back individual lines as they become complete.
|
3
|
+
class LineBuffer
|
4
|
+
def initialize
|
5
|
+
@buffer = ''
|
6
|
+
end
|
7
|
+
|
8
|
+
# Appends a new piece of ASCII data to this buffer. Any lines that are now complete will be yielded back.
|
9
|
+
#
|
10
|
+
# @param [String] data The new piece of ASCII data.
|
11
|
+
def process(data)
|
12
|
+
@buffer << data
|
13
|
+
|
14
|
+
lines = @buffer.split "\n"
|
15
|
+
@buffer = @buffer.end_with?("\n") ? '' : lines.pop
|
16
|
+
|
17
|
+
lines.each do |line|
|
18
|
+
yield line.strip
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This error class is raised by {Session} when a request to the Lightstreamer API fails with a Lightstreamer-specific
|
3
|
+
# error code and error message.
|
4
|
+
class ProtocolError < StandardError
|
5
|
+
# @return [String] A description of the Lightstreamer error that occurred.
|
6
|
+
attr_reader :error
|
7
|
+
|
8
|
+
# @return [Fixnum] The numeric code of the Lightstreamer error.
|
9
|
+
attr_reader :code
|
10
|
+
|
11
|
+
# Initializes this protocol error with the specific message and code.
|
12
|
+
#
|
13
|
+
# @param [String] error The error description.
|
14
|
+
# @param [Integer] code The numeric code for the error.
|
15
|
+
def initialize(error, code)
|
16
|
+
@error = error.to_s
|
17
|
+
@http_code = code.to_i
|
18
|
+
|
19
|
+
super "Lightstreamer error: #{@error}, code: #{code}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This error class is raised by {Session} when a request to the Lightstreamer API fails.
|
3
|
+
class RequestError < StandardError
|
4
|
+
# @return [String] A description of the error that occurred when the request was attempted.
|
5
|
+
attr_reader :error
|
6
|
+
|
7
|
+
# @return [Fixnum] The HTTP code that was returned, or `nil` if unknown.
|
8
|
+
attr_reader :http_code
|
9
|
+
|
10
|
+
# Initializes this request error with a message and an HTTP code.
|
11
|
+
#
|
12
|
+
# @param [String] error The error description.
|
13
|
+
# @param [Integer] http_code The HTTP code for the request failure, if known.
|
14
|
+
def initialize(error, http_code = nil)
|
15
|
+
@error = error.to_s
|
16
|
+
@http_code = http_code ? http_code.to_i : nil
|
17
|
+
|
18
|
+
super "Request error: #{error}#{http_code ? ", http code: #{http_code}" : ''}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# This class is responsible for managing a Lightstreamer session, and along with the {Subscription} class is the
|
3
|
+
# primary interface for working with Lightstreamer.
|
4
|
+
class Session
|
5
|
+
# @return [String] The URL of the Lightstreamer server to connect to. Set by {#initialize}.
|
6
|
+
attr_reader :server_url
|
7
|
+
|
8
|
+
# @return [String] The username to connect to the Lightstreamer server with. Set by {#initialize}.
|
9
|
+
attr_reader :username
|
10
|
+
|
11
|
+
# @return [String] The password to connect to the Lightstreamer server with. Set by {#initialize}.
|
12
|
+
attr_reader :password
|
13
|
+
|
14
|
+
# @return [String] The name of the adapter set to request from the Lightstreamer server. Set by {#initialize}.
|
15
|
+
attr_reader :adapter_set
|
16
|
+
|
17
|
+
# Initializes this new Lightstreamer session with the passed options.
|
18
|
+
#
|
19
|
+
# @param [Hash] options The options to create the session with.
|
20
|
+
# @option options [String] :server_url The URL of the Lightstreamer server. Required.
|
21
|
+
# @option options [String] :username The username to connect to the server with. Optional.
|
22
|
+
# @option options [String] :password The password to connect to the server with. Optional.
|
23
|
+
# @option options [String] :adapter_set The name of the adapter set to request from the server. Optional.
|
24
|
+
def initialize(options = {})
|
25
|
+
@subscriptions = []
|
26
|
+
@subscriptions_mutex = Mutex.new
|
27
|
+
|
28
|
+
@server_url = options.fetch :server_url
|
29
|
+
@username = options[:username]
|
30
|
+
@password = options[:password]
|
31
|
+
@adapter_set = options[:adapter_set]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates a new Lightstreamer session using the details passed to {#initialize}. If an error occurs then
|
35
|
+
# {ProtocolError} will be raised.
|
36
|
+
def connect
|
37
|
+
@stream_connection = StreamConnection.new self
|
38
|
+
@subscriptions = []
|
39
|
+
|
40
|
+
first_line = @stream_connection.read_line
|
41
|
+
|
42
|
+
if first_line == 'OK'
|
43
|
+
@session_id = read_session_id
|
44
|
+
create_control_connection
|
45
|
+
create_processing_thread
|
46
|
+
elsif first_line == 'ERROR'
|
47
|
+
handle_connection_error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Subscribes this Lightstreamer session to the specified subscription.
|
52
|
+
#
|
53
|
+
# @param [Subscription] subscription The new subscription to subscribe to.
|
54
|
+
def subscribe(subscription)
|
55
|
+
subscription.clear_data
|
56
|
+
|
57
|
+
@subscriptions_mutex.synchronize { @subscriptions << subscription }
|
58
|
+
|
59
|
+
begin
|
60
|
+
@control_connection.execute table: subscription.id, operation: :add, mode: subscription.mode,
|
61
|
+
items: subscription.items, fields: subscription.fields,
|
62
|
+
adapter: subscription.adapter
|
63
|
+
rescue
|
64
|
+
@subscriptions_mutex.synchronize { @subscriptions.delete subscription }
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Unsubscribes this Lightstreamer session from the specified subscription.
|
70
|
+
#
|
71
|
+
# @param [Subscription] subscription The existing subscription to unsubscribe from.
|
72
|
+
def unsubscribe(subscription)
|
73
|
+
@subscriptions_mutex.synchronize do
|
74
|
+
raise ArgumentError, 'Unknown subscription' unless @subscriptions.detect subscription
|
75
|
+
end
|
76
|
+
|
77
|
+
@control_connection.execute table: subscription.id, operation: :delete
|
78
|
+
|
79
|
+
@subscriptions_mutex.synchronize { @subscriptions.delete subscription }
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Parses the next line of data from the stream connection as the session ID and returns it.
|
85
|
+
def read_session_id
|
86
|
+
@stream_connection.read_line.match(/^SessionId:(.*)$/).captures.first
|
87
|
+
end
|
88
|
+
|
89
|
+
# Attempts to parses the next line of data from the stream connection as a custom control address and then uses this
|
90
|
+
# address to create the control connection. Note that the control address is optional and if it is absent then
|
91
|
+
# {#server_url} will be used instead of a custom control address.
|
92
|
+
def create_control_connection
|
93
|
+
match = @stream_connection.read_line.match(/^ControlAddress:(.*)$/)
|
94
|
+
control_address = (match && match.captures.first) || server_url
|
95
|
+
|
96
|
+
# The rest of the contents in the header is ignored, so read up until the blank line that marks its ending
|
97
|
+
loop { break if @stream_connection.read_line == '' }
|
98
|
+
|
99
|
+
# If the control URL doesn't have a schema then use the same schema as the server URL
|
100
|
+
control_address = "#{URI(server_url).scheme}://#{control_address}" unless control_address.start_with? 'http'
|
101
|
+
|
102
|
+
@control_connection = ControlConnection.new @session_id, control_address
|
103
|
+
end
|
104
|
+
|
105
|
+
# Starts the processing thread that reads and processes incoming data from the stream connection.
|
106
|
+
def create_processing_thread
|
107
|
+
@processing_thread = Thread.new do
|
108
|
+
begin
|
109
|
+
loop do
|
110
|
+
process_stream_data @stream_connection.read_line
|
111
|
+
end
|
112
|
+
rescue StandardError => error
|
113
|
+
warn "Lightstreamer: exception in processing thread: #{error}"
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Processes a single line of incoming stream data by passing it to all the active subscriptions until one
|
120
|
+
# successfully processes it. This method is always run on the processing thread.
|
121
|
+
def process_stream_data(line)
|
122
|
+
was_processed = @subscriptions_mutex.synchronize do
|
123
|
+
@subscriptions.detect do |subscription|
|
124
|
+
subscription.process_stream_data line
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
warn "Lightstreamer: unprocessed stream data '#{line}'" unless was_processed
|
129
|
+
end
|
130
|
+
|
131
|
+
# Handles a failure to establish a stream connection by reading off the error code and error message then raising
|
132
|
+
# a {ProtocolError}.
|
133
|
+
def handle_connection_error
|
134
|
+
error_code = @stream_connection.read_line
|
135
|
+
error_message = @stream_connection.read_line
|
136
|
+
|
137
|
+
@stream_connection = nil
|
138
|
+
@control_connection = nil
|
139
|
+
|
140
|
+
raise ProtocolError, error_message, error_code
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Manages a long-running Lightstreamer connection that handles incoming streaming data on a separate thread and
|
3
|
+
# makes it available for consumption via the {#read_line} method.
|
4
|
+
class StreamConnection
|
5
|
+
# Establishes a new stream connection using the authentication details from the passed session.
|
6
|
+
#
|
7
|
+
# @param [Session] session The session to create a stream connection for.
|
8
|
+
def initialize(session)
|
9
|
+
@session = session
|
10
|
+
@queue = Queue.new
|
11
|
+
|
12
|
+
create_stream
|
13
|
+
create_stream_thread
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reads the next line of streaming data. This method blocks the calling thread until a line of data is available.
|
17
|
+
def read_line
|
18
|
+
@queue.pop
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_stream
|
24
|
+
@stream = Net::HTTP.new stream_uri.host, stream_uri.port
|
25
|
+
@stream.use_ssl = true if stream_uri.port == 443
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_stream_thread
|
29
|
+
@thread = Thread.new do
|
30
|
+
begin
|
31
|
+
connect_stream_and_queue_data
|
32
|
+
|
33
|
+
warn 'Lightstreamer: stream connection closed'
|
34
|
+
rescue StandardError => error
|
35
|
+
warn "Lightstreamer: exception in stream thread: #{error}"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initiate_stream_post_request
|
42
|
+
Net::HTTP::Post.new(stream_uri.path).tap do |request|
|
43
|
+
request.body = stream_create_parameters
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def connect_stream_and_queue_data
|
48
|
+
@stream.request initiate_stream_post_request do |response|
|
49
|
+
buffer = LineBuffer.new
|
50
|
+
response.read_body do |data|
|
51
|
+
buffer.process data do |line|
|
52
|
+
@queue.push line unless ignore_line? line
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def stream_uri
|
59
|
+
URI.join @session.server_url, '/lightstreamer/create_session.txt'
|
60
|
+
end
|
61
|
+
|
62
|
+
def stream_create_parameters
|
63
|
+
params = {
|
64
|
+
LS_op2: 'create',
|
65
|
+
LS_cid: 'mgQkwtwdysogQz2BJ4Ji kOj2Bg',
|
66
|
+
LS_user: @session.username,
|
67
|
+
LS_password: @session.password
|
68
|
+
}
|
69
|
+
|
70
|
+
params[:LS_adapter_set] = @session.adapter_set if @session.adapter_set
|
71
|
+
|
72
|
+
URI.encode_www_form params
|
73
|
+
end
|
74
|
+
|
75
|
+
def ignore_line?(line)
|
76
|
+
line =~ /^(PROBE|Preamble:.*)$/
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module Lightstreamer
|
2
|
+
# Describes a subscription that can be bound to a Lightstreamer session in order to consume its data. A subscription
|
3
|
+
# is described by the options passed to {#initialize}. Incoming data can be consumed by registering an asynchronous
|
4
|
+
# data callback using {#add_data_callback}, or by polling {#retrieve_item_data}. Subscriptions start receiving data
|
5
|
+
# only once they are attached to a Lightstreamer session using {Session#subscribe}.
|
6
|
+
class Subscription
|
7
|
+
# @return [Fixnum] The unique identification number of this subscription. This is used to identify the subscription
|
8
|
+
# in incoming Lightstreamer data.
|
9
|
+
attr_reader :id
|
10
|
+
|
11
|
+
# @return [Array<String>] The names of the items to subscribe to.
|
12
|
+
attr_reader :items
|
13
|
+
|
14
|
+
# @return [Array<String>] The names of the fields to subscribe to on the items.
|
15
|
+
attr_reader :fields
|
16
|
+
|
17
|
+
# @return [:distinct, :merge] The operation mode of this subscription.
|
18
|
+
attr_reader :mode
|
19
|
+
|
20
|
+
# @return [String] The name of the data adapter from the Lightstreamer session's adapter set that should be used.
|
21
|
+
# If `nil` then the default data adapter will be used.
|
22
|
+
attr_reader :adapter
|
23
|
+
|
24
|
+
# Initializes a new Lightstreamer subscription with the specified options. This can then be passed to
|
25
|
+
# {Session#subscribe} to activate the subscription on a Lightstreamer session.
|
26
|
+
#
|
27
|
+
# @param [Hash] options The options to create the subscription with.
|
28
|
+
# @option options [Array<String>] :items The names of the items to subscribe to.
|
29
|
+
# @option options [Array<String>] :fields The names of the fields to subscribe to on the items.
|
30
|
+
# @option options [:distinct, :merge] :mode The operation mode of this subscription.
|
31
|
+
# @option options [String] :adapter The name of the data adapter from the Lightstreamer session's adapter set that
|
32
|
+
# should be used. If `nil` then the default data adapter will be used.
|
33
|
+
def initialize(options)
|
34
|
+
@id = self.class.next_id
|
35
|
+
|
36
|
+
@items = Array(options.fetch(:items))
|
37
|
+
@fields = Array(options.fetch(:fields))
|
38
|
+
@mode = options.fetch(:mode).to_sym
|
39
|
+
@adapter = options[:adapter]
|
40
|
+
|
41
|
+
@data_mutex = Mutex.new
|
42
|
+
clear_data
|
43
|
+
|
44
|
+
@data_callbacks = []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Clears all current data stored for this subscription. New data will continue to be processed as it becomes
|
48
|
+
# available.
|
49
|
+
def clear_data
|
50
|
+
@data_mutex.synchronize do
|
51
|
+
@data = (0...items.size).map { { distinct: [], merge: {} }.fetch(mode) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Clears the current data stored for the specified item. This is important to do when {#mode} is `:distinct` as
|
56
|
+
# otherwise the incoming data will build up indefinitely.
|
57
|
+
#
|
58
|
+
# @param [String] item_name The name of the item to clear the current data of.
|
59
|
+
def clear_data_for_item(item_name)
|
60
|
+
index = @items.index item_name
|
61
|
+
raise ArgumentError, 'Unrecognized item name' unless index
|
62
|
+
|
63
|
+
@data_mutex.synchronize do
|
64
|
+
@data[index] = { distinct: [], merge: {} }.fetch(mode)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Adds the passed block to the list of callbacks that will be run when new data for this subscription arrives. The
|
69
|
+
# block will be called on a worker thread and so the code that is run by the block must be thread-safe. The
|
70
|
+
# arguments passed to the block are `|subscription, item_name, item_data, new_values|`.
|
71
|
+
#
|
72
|
+
# @param [Proc] block The callback block that is to be run when new data arrives.
|
73
|
+
#
|
74
|
+
# @return [Proc] The same `Proc` object that was passed to this method. This can be used to remove this data
|
75
|
+
# callback at a later stage using {#remove_data_callback}.
|
76
|
+
def add_data_callback(&block)
|
77
|
+
raise ArgumentError, 'Data callbacks must take four arguments' unless block.arity == 4
|
78
|
+
|
79
|
+
@data_mutex.synchronize { @data_callbacks << block }
|
80
|
+
|
81
|
+
block
|
82
|
+
end
|
83
|
+
|
84
|
+
# Removes a data callback that was added by {#add_data_callback}.
|
85
|
+
#
|
86
|
+
# @param [Proc] block The data callback block to remove.
|
87
|
+
def remove_data_callback(block)
|
88
|
+
@data_mutex.synchronize { @data_callbacks.delete block }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the current data of one of this subscription's items.
|
92
|
+
#
|
93
|
+
# @param [String] item_name The name of the item to return the current data for.
|
94
|
+
#
|
95
|
+
# @return [Hash, Array] The item data. Will be a `Hash` if {#mode} is `:merge`, and an `Array` if {#mode} is
|
96
|
+
# `:distinct`.
|
97
|
+
def retrieve_item_data(item_name)
|
98
|
+
index = @items.index item_name
|
99
|
+
raise ArgumentError, 'Unrecognized item name' unless index
|
100
|
+
|
101
|
+
@data_mutex.synchronize do
|
102
|
+
@data[index].dup
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Processes a line of stream data if it is relevant to this subscription. This method is thread-safe and is intended
|
107
|
+
# to be called by the session's processing thread.
|
108
|
+
#
|
109
|
+
# @param [String] line The line of stream data to process.
|
110
|
+
#
|
111
|
+
# @return [Boolean] Whether the passed line of stream data was relevant to this subscription and was successfully
|
112
|
+
# processed by it.
|
113
|
+
def process_stream_data(line)
|
114
|
+
item_index, values = parse_stream_data line
|
115
|
+
return false unless item_index
|
116
|
+
|
117
|
+
@data_mutex.synchronize do
|
118
|
+
data = @data[item_index]
|
119
|
+
|
120
|
+
data << values if mode == :distinct
|
121
|
+
data.merge!(values) if mode == :merge
|
122
|
+
|
123
|
+
@data_callbacks.each { |callback| callback.call self, @items[item_index], data, values }
|
124
|
+
end
|
125
|
+
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the next unique ID to use for a new subscription.
|
130
|
+
#
|
131
|
+
# @return [Fixnum]
|
132
|
+
def self.next_id
|
133
|
+
@next_id ||= 0
|
134
|
+
@next_id += 1
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Attempts to parse an line of stream data.
|
140
|
+
#
|
141
|
+
# @param [String] line The line of stream data to parse.
|
142
|
+
#
|
143
|
+
# @return [Array] The first value is the item index, and the second is a hash of the values contained in the stream
|
144
|
+
# data. Will be nil if the stream data was not able to be parsed.
|
145
|
+
def parse_stream_data(line)
|
146
|
+
match = line.match stream_data_regex
|
147
|
+
return unless match
|
148
|
+
|
149
|
+
item_index = match.captures[0].to_i - 1
|
150
|
+
return unless item_index < @items.size
|
151
|
+
|
152
|
+
[item_index, parse_values(match.captures[1..-1])]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the regular expression that will match a single line of data in the incoming stream that is relevant to
|
156
|
+
# this subscription. The ID at the beginning must match, as well as the number of fields.
|
157
|
+
#
|
158
|
+
# @return [Regexp]
|
159
|
+
def stream_data_regex
|
160
|
+
Regexp.new "^#{id},(\\d+)#{'\|(.*)' * fields.size}"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Parses an array of values from an incoming line of stream data into a hash where the keys are the field names
|
164
|
+
# defined for this subscription.
|
165
|
+
#
|
166
|
+
# @param [String] values The raw values from the incoming stream data.
|
167
|
+
#
|
168
|
+
# @return [Hash] The parsed values as a hash where the keys are the field names of this subscription.
|
169
|
+
def parse_values(values)
|
170
|
+
hash = {}
|
171
|
+
|
172
|
+
values.each_with_index do |value, index|
|
173
|
+
next if value == ''
|
174
|
+
|
175
|
+
# These conversions are specified in the Lightstreamer specification
|
176
|
+
value = '' if value == '$'
|
177
|
+
value = nil if value == '#'
|
178
|
+
value = value[1..-1] if value =~ /^($|#)/
|
179
|
+
|
180
|
+
hash[fields[index]] = convert_utf16_characters value
|
181
|
+
end
|
182
|
+
|
183
|
+
hash
|
184
|
+
end
|
185
|
+
|
186
|
+
# Non-ASCII characters are transmitted as UTF-16 escape sequences in the form '\uXXXX' or '\uXXXX\uYYYY'. This
|
187
|
+
# method is respnsible for converting such escape sequences into native Unicode characters.
|
188
|
+
#
|
189
|
+
# @todo Implement this method.
|
190
|
+
def convert_utf16_characters(value)
|
191
|
+
value
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
metadata
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lightstreamer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richard Viney
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rest-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.19'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.19'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: factory_girl
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: github-markup
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: redcarpet
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.3'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.3'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.5'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.5'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec-mocks
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.5'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.5'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.41'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.41'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: yard
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.9'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.9'
|
167
|
+
description:
|
168
|
+
email: richard.viney@gmail.com
|
169
|
+
executables:
|
170
|
+
- lightstreamer
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- CHANGELOG.md
|
175
|
+
- LICENSE.md
|
176
|
+
- README.md
|
177
|
+
- bin/lightstreamer
|
178
|
+
- lib/lightstreamer.rb
|
179
|
+
- lib/lightstreamer/cli/main.rb
|
180
|
+
- lib/lightstreamer/cli/stream_command.rb
|
181
|
+
- lib/lightstreamer/control_connection.rb
|
182
|
+
- lib/lightstreamer/line_buffer.rb
|
183
|
+
- lib/lightstreamer/protocol_error.rb
|
184
|
+
- lib/lightstreamer/request_error.rb
|
185
|
+
- lib/lightstreamer/session.rb
|
186
|
+
- lib/lightstreamer/stream_connection.rb
|
187
|
+
- lib/lightstreamer/subscription.rb
|
188
|
+
- lib/lightstreamer/version.rb
|
189
|
+
homepage: https://github.com/rviney/lightstreamer
|
190
|
+
licenses:
|
191
|
+
- MIT
|
192
|
+
metadata: {}
|
193
|
+
post_install_message:
|
194
|
+
rdoc_options: []
|
195
|
+
require_paths:
|
196
|
+
- lib
|
197
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '2.0'
|
202
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
requirements: []
|
208
|
+
rubyforge_project:
|
209
|
+
rubygems_version: 2.5.1
|
210
|
+
signing_key:
|
211
|
+
specification_version: 4
|
212
|
+
summary: Ruby client for accessing a Lightstreamer server.
|
213
|
+
test_files: []
|