x-streams 1.0.0
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/.yardopts +8 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/lib/x/streams/api.rb +41 -0
- data/lib/x/streams/callback_error.rb +53 -0
- data/lib/x/streams/error.rb +34 -0
- data/lib/x/streams/reconnect_handler.rb +322 -0
- data/lib/x/streams/rules_rejected.rb +84 -0
- data/lib/x/streams/stopper.rb +366 -0
- data/lib/x/streams/stream_error.rb +96 -0
- data/lib/x/streams/stream_parser.rb +182 -0
- data/lib/x/streams/stream_rule.rb +182 -0
- data/lib/x/streams/stream_rules.rb +153 -0
- data/lib/x/streams/streaming_client.rb +544 -0
- data/lib/x/streams/validator.rb +119 -0
- data/lib/x/streams/version.rb +24 -0
- data/lib/x/streams.rb +9 -0
- data/sig/manifest.yaml +6 -0
- data/sig/x-streams.rbs +87 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 351c36af5826dec08a5ca7c014b7b34d18062bbe1de19b0a9593290c075e7f7d
|
|
4
|
+
data.tar.gz: c3473196d06dad56b10422b60859074fde027611fda1c99038e4461d2422487e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f1570d57cec22227dc0ba01f18649c5874aca5422ea10953415379a17029c13d5e8a5b166c3eb3a9bdade21c899c028b0ee1427067690d7bce9d4a20e2b924f1
|
|
7
|
+
data.tar.gz: f05079b5e9b1783711f7938d4f5347376205ec7208b703d6d20617d32fe38c12716922459845cfa9c8f00cf88f5e297ce096581c7d77db42a04b69c42eb6f405
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `x-streams` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
`x-streams` is released in lockstep with the other gems of the [x-ruby](https://github.com/sferik/x-ruby) repository, at one version across `x-core`, `x-uploads`, `x-streams`, `x-resources`, and `x`. This file holds the changes to the streams; [the changelog of the repository](https://github.com/sferik/x-ruby/blob/main/CHANGELOG.md) holds the changes to every gem.
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
The first release of `x-streams`, which 1.0.0 split out of the `x` gem. The entries below are the changes since `x` 0.19, the last release before the split; see [UPGRADING.md](https://github.com/sferik/x-ruby/blob/main/UPGRADING.md) for the changes that code written for 0.19 needs. It requires Ruby 3.4 or later.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
* Split the `x` gem into five gems released in lockstep, at one version
|
|
16
|
+
* `x-core` (the HTTP client), `x-uploads` (uploads), `x-streams` (streams and their rules), `x-resources` (resources).
|
|
17
|
+
* `x` is a meta-gem that depends on all four and mixes their object, upload, and streaming methods into `X::Client`.
|
|
18
|
+
* `x-core` declares `X::Error`, the base class of every error the gems raise.
|
|
19
|
+
* Public classes sit directly under `X`; `X::Resources::Error`, `X::Uploads::Error`, and `X::Streams::Error` catch one gem's.
|
|
20
|
+
* `x` depends on exactly its own version of the other four; `x-uploads`, `x-streams`, and `x-resources` each depend on `x-core` with `>= 1.0.0, < 2`, so a later 1.x of `x-core` installs beside them, and never an earlier one.
|
|
21
|
+
* Stream with `X::StreamingClient`, which `X::Client#streaming` builds from a client
|
|
22
|
+
* It shares the client's credentials, base URL, parsing classes, and `on_response`, and keeps its own settings for life.
|
|
23
|
+
* `X::Client#streaming` takes `read_timeout:`, `max_reconnects:`, and `on_reconnect:`; for other settings, stream from `client.with(...)`.
|
|
24
|
+
* `stream` raises `ArgumentError` when given no block, before it opens the stream.
|
|
25
|
+
* Give a client `streaming` with `X::Streams::API`, which `x` includes into `X::Client`
|
|
26
|
+
* With `x-core` alone, call `X::Client.include(X::Streams::API)`, or build one with `X::StreamingClient.new(client)`.
|
|
27
|
+
* A streaming client opens each stream with `X::Client#get_stream` of `x-core`.
|
|
28
|
+
* Stop the streams of a streaming client from any thread, or the trap of a signal, with `X::StreamingClient#stop`
|
|
29
|
+
* Each stops the next time it waits on the API, at once for an idle stream or one waiting to reconnect.
|
|
30
|
+
* A stream a Fiber scheduler runs, as in an `Async` task, stops once its block next returns, at the next keep-alive, or within a second as it waits to reconnect.
|
|
31
|
+
* A stopped streaming client stays stopped: a stream asked of it later returns nil at once, without a request.
|
|
32
|
+
* Each stopped `stream` call returns nil, `stop` returns nil, and `stopped?` tells a stopped streaming client.
|
|
33
|
+
* A block, `on_response`, or `on_reconnect` running at that moment runs to its end first.
|
|
34
|
+
* `X::Client#streaming` builds a new streaming client each time, so keep the one you stop in a variable.
|
|
35
|
+
* Stop a stream from its block: `break` stops it and returns its value, and `throw` unwinds past it
|
|
36
|
+
* An error raised by the block, by `on_response`, or by the `object_class` stops the stream and reaches the caller.
|
|
37
|
+
* That includes `StopIteration`, and a `JSON::ParserError`, which is not taken for a line that is not JSON.
|
|
38
|
+
* Read and change the rules of the filtered stream with `rules`, `add_rules`, and `delete_rules` on `X::StreamingClient`
|
|
39
|
+
* They authenticate as the app, as a stream does; `rules` takes `params:` and reads every page.
|
|
40
|
+
* A rule to add is an `X::StreamRule`, a Hash of a `value` and `tag`, or a String; anything else raises `ArgumentError`.
|
|
41
|
+
* A rule is deleted by its `id` (from an `X::StreamRule`, a Hash, an Integer, or an `X::MatchingRule`, and nothing else that answers `id`) or by its value.
|
|
42
|
+
* So `delete_rules(post.matching_rules)` deletes the rules a post of `x-resources` matched.
|
|
43
|
+
* `dry_run: true` has the API check the rules and change none; no rules send no request.
|
|
44
|
+
* `add_rules` returns the rules added; `delete_rules` returns the number deleted.
|
|
45
|
+
* A rule the app already has, a `DuplicateRule`, raises nothing and is not among the rules returned; `rules` reads it, with its `id` and `tag`.
|
|
46
|
+
* Yield each problem the API reports of the rules it did not add or delete as an `X::Problem`, or raise `X::RulesRejected` without a block
|
|
47
|
+
* A block is yielded a `DuplicateRule` too; without one, only the rules X rejected raise, so `add_rules` can run at every boot.
|
|
48
|
+
* A rule X rejects, such as an invalid one, may keep it from adding any, and it reports that one alone; the rules returned say which were added.
|
|
49
|
+
* `X::RulesRejected` holds the `problems`, and what the method would have returned as `added` or `deleted_count`.
|
|
50
|
+
* Add `X::StreamRule`, a frozen value of a rule's `value`, `tag`, and `id`, an Integer or nil for a rule to add
|
|
51
|
+
* An `id` is an Integer that is not negative or a String of digits alone; anything else raises `ArgumentError`.
|
|
52
|
+
* It compares and hashes by all three, reads as a Hash with `to_h`, and matches a pattern of them.
|
|
53
|
+
* It writes itself with Marshal and YAML in a versioned format that every 1.x release reads back.
|
|
54
|
+
* A format it does not read raises `X::UnsupportedFormat`.
|
|
55
|
+
* Stream with app-only authentication from a client that signs with OAuth 1.0a, since the stream endpoints refuse it
|
|
56
|
+
* An OAuth 2.0 user client without app credentials streams, and reads and changes rules, as the user.
|
|
57
|
+
* X refuses that with 403, which raises `X::Forbidden`, not `X::UnsupportedOperation` before connecting.
|
|
58
|
+
* Raise `X::StreamError`, an `X::Streams::Error`, for a stream line that holds errors and no data
|
|
59
|
+
* It holds the line's `problems`, reads the stream's `http_method` and `uri`, and names them in its message.
|
|
60
|
+
* A line of `operational-disconnect`s alone reconnects, then raises once no reconnects are left; others raise at once.
|
|
61
|
+
* `X::Problem#disconnect?` tells an `operational-disconnect` apart, and `on_response` is passed the line first.
|
|
62
|
+
* Reconnect a stream that ends, drops, or delivers a line that is not JSON, backing off as X recommends
|
|
63
|
+
* It reconnects up to `max_reconnects` times in a row, unlimited by default.
|
|
64
|
+
* An object or keep-alive read from a connection open for a minute resets the count and the backoff; an earlier one resets neither.
|
|
65
|
+
* So a stream whose connections each deliver an object and drop backs off, and runs out of reconnects, rather than reconnect at once without end.
|
|
66
|
+
* A line that is not JSON raises `X::InvalidResponse`, and an ended stream `X::NetworkError`, once no reconnects are left.
|
|
67
|
+
* A rate limit backs off from a minute, doubling each attempt, or waits for a later reset.
|
|
68
|
+
* A wait past `max_rate_limit_wait`, or the project's usage cap, raises `X::TooManyRequests` at once.
|
|
69
|
+
* A server error, a 408, or a 409 backs off from 5 seconds, doubling up to 320 seconds, or waits longer for its `Retry-After`.
|
|
70
|
+
* A `Retry-After` past `max_rate_limit_wait` raises the error at once.
|
|
71
|
+
* What arrived of a line the stream ended within is dropped, neither parsed nor passed to `on_response`.
|
|
72
|
+
* An error `on_response` raises for a failed response stops the stream and reaches the caller, as one for an object does.
|
|
73
|
+
* A certificate that does not verify raises its `X::NetworkError` at once, since it would not verify on the next attempt.
|
|
74
|
+
* Report each reconnect of a stream to `on_reconnect:`, a callable passed the error and the seconds it waits
|
|
75
|
+
* It is passed an error every time, never nil, and is called with those two arguments and no others throughout 1.x.
|
|
76
|
+
* It can call `stop` to give up, as on a host that never resolves, and the stream returns nil.
|
|
77
|
+
* An error it raises stops the stream and reaches the caller.
|
|
78
|
+
* Read a stream with the `read_timeout` of the streaming client, 30 seconds by default
|
|
79
|
+
* Raise `ArgumentError` from `X::Client#streaming` and `X::StreamingClient.new` for an invalid setting
|
|
80
|
+
* `max_reconnects` must be an Integer of at least 0, or `Float::INFINITY`.
|
|
81
|
+
* `read_timeout` must be a finite number of seconds of at least 25, or nil, since X sends a keep-alive every 20 seconds.
|
|
82
|
+
* `stream` raises it too for an invalid `array_class`, `object_class`, or endpoint, before it opens the stream.
|
|
83
|
+
* Rescue the errors of `x-streams`, `X::StreamError` and `X::RulesRejected`, with `X::Streams::Error`, an `X::Error`
|
|
84
|
+
* Add `X::Streams.gem_version`, which returns `X::Streams::VERSION` as a `Gem::Version`
|
|
85
|
+
* Add `X::StreamingClient::DEFAULT_READ_TIMEOUT` and `X::StreamingClient::DEFAULT_MAX_RECONNECTS`
|
|
86
|
+
* Build `X::StreamError` and `X::RulesRejected` with public constructors, so code that rescues one can be tested
|
|
87
|
+
* Each takes an optional message and `problems:`, beside `http_method:` and `uri:`, or `added:` and `deleted_count:`.
|
|
88
|
+
* `raise X::StreamError, "dropped"` raises it as any other exception is raised.
|
|
89
|
+
* Raise `TypeError` from `Marshal.dump`, `YAML.dump`, `as_json`, and `to_json` of a streaming client
|
|
90
|
+
* They would write the credentials of its client in the clear.
|
|
91
|
+
* Name the internals of `x-streams` under `X::Streams`, as private constants marked `@api private`
|
|
92
|
+
* They are `StreamParser`, `ReconnectHandler`, `StreamRules`, `Validator`, `Stopper`, and `CallbackError`.
|
|
93
|
+
* Ship this changelog with `x-streams`, which the `changelog_uri` of its gemspec names
|
|
94
|
+
* Ship a `.yardopts` with `x-streams`, so its documentation on rubydoc.info leaves out the private API
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
* Move `stream` from `X::Client` to `X::StreamingClient`, so that the client carries no streaming settings
|
|
98
|
+
|
|
99
|
+
[1.0.0]: https://github.com/sferik/x-ruby/releases/tag/v1.0.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Erik Berlin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# x-streams
|
|
2
|
+
|
|
3
|
+
Streaming for the [`x` gem](https://github.com/sferik/x-ruby), built on the HTTP client in [`x-core`](https://github.com/sferik/x-ruby/tree/main/x-core).
|
|
4
|
+
|
|
5
|
+
* `X::StreamingClient` reads the sample and filtered streams of the X API, a post at a time, and reconnects a stream that drops, backing off as X recommends.
|
|
6
|
+
* It reads, adds, and deletes the rules of the filtered stream, each an `X::StreamRule`.
|
|
7
|
+
|
|
8
|
+
Its only dependency is `x-core`. Installing [`x`](https://rubygems.org/gems/x) installs this gem too, and `require "x"` gives every `X::Client` its `streaming` method.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
`x-streams` requires Ruby 3.4 or later.
|
|
13
|
+
|
|
14
|
+
bundle add x-streams
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
require "x/core"
|
|
20
|
+
require "x/streams"
|
|
21
|
+
|
|
22
|
+
X::Client.include(X::Streams::API)
|
|
23
|
+
|
|
24
|
+
client = X::Client.new(bearer_token: "INSERT YOUR BEARER TOKEN HERE")
|
|
25
|
+
|
|
26
|
+
# A rule the app already has is not added again, and raises nothing, so the rules an app needs are added each time
|
|
27
|
+
# it boots; the rules returned are those added, and rules reads them all, each with its id and tag
|
|
28
|
+
client.streaming.add_rules([{value: "ruby -is:retweet", tag: "ruby"}, "crystal"])
|
|
29
|
+
client.streaming.rules # => [#<X::StreamRule id=1 value="ruby -is:retweet" tag="ruby">, ...]
|
|
30
|
+
client.streaming.stream("tweets/search/stream") { |post| puts post["data"]["text"] }
|
|
31
|
+
|
|
32
|
+
# A stream runs until its block, or stop, stops it: break to stop it and return a value, throw to unwind further out, or
|
|
33
|
+
# raise to stop it with an error the caller sees
|
|
34
|
+
first = client.streaming.stream("tweets/search/stream") { |post| break post }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Another thread, or the trap of a signal, stops every stream a streaming client runs with `stop`, and each stream it stops returns nil. A stream that a Fiber scheduler runs, as in an `Async` task, is stopped once its block next returns, at the next keep-alive, within 20 seconds, or within a second as it waits to reconnect. A stopped streaming client stays stopped, as `stopped?` tells: a stream asked of it later returns nil without a request. Keep the streaming client you stop in a variable, since each call to `client.streaming` builds a new one.
|
|
38
|
+
|
|
39
|
+
`X::Client.include(X::Streams::API)` gives a client `streaming`, as `x` does, and a type checker needs the include declared too, as the `sig/x.rbs` of `x` declares it, with `class X::Client` and `include X::Streams::API` in a signature of your own; without the include, build a streaming client of a client with `X::StreamingClient.new(client)`, which takes the same `read_timeout:`, `max_reconnects:`, and `on_reconnect:`, a callable passed the error and the wait of each reconnect.
|
|
40
|
+
|
|
41
|
+
A streaming client opens each stream with `X::Client#get_stream` of `x-core`, the public method of a client that sends a GET whose body is read as it arrives, from an app-only copy of the client whose `read_timeout` is the stream's (a client that authenticates with OAuth 2.0 as a user and holds no credentials of the app streams as the user, which X refuses with `X::Forbidden`), so a stream carries the credentials, headers, and proxy of the client as any request does. It reads each line of the stream into the object it delivers, passes each to the client's `on_response` as an `X::Response`, and reconnects after a dropped connection, a server error, a rate limit, or an `operational-disconnect`, as the [`x` README](https://github.com/sferik/x-ruby#streaming) describes.
|
|
42
|
+
|
|
43
|
+
A stream reconnects at once the first time it drops, then waits longer before each reconnect in a row, up to `max_reconnects` of them. An object or keep-alive read from a connection that has been open for a minute starts the waits and the count over; one read from a younger connection starts neither over, so a stream whose connections each deliver something and drop backs off, and runs out of reconnects, rather than reconnect at once without end.
|
|
44
|
+
|
|
45
|
+
`X::StreamError`, which a line of a stream that holds errors and no data raises, and `X::RulesRejected`, which `add_rules` and `delete_rules` raise for the rules the API rejected when no block takes them (a rule the app already has is not one of them: `add_rules` raises nothing for it, yields its `DuplicateRule` to a block, and returns the rules it added alone, so read a rule the app already had, with its `id` and `tag`, with `rules`), descend from `X::Streams::Error`, which descends from `X::Error`, so `rescue X::Streams::Error` catches the errors x-streams raises of its own, and `rescue X::Error` every failure of a stream.
|
|
46
|
+
|
|
47
|
+
With `x-resources` loaded as well, a stream builds each post as an `X::Post` given `object_class: X::Post`, whose `matching_rules` are the rules of the filtered stream it matched.
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
This gem has its own `Gemfile`, `Steepfile`, signatures, test suite, and mutation config. It uses the `x-core` in this repository and does not load `x-resources` or `x-uploads`:
|
|
52
|
+
|
|
53
|
+
bundle install
|
|
54
|
+
bundle exec rake test
|
|
55
|
+
bundle exec rake mutant
|
|
56
|
+
bundle exec rake steep
|
|
57
|
+
bundle exec rake yardstick
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "streaming_client"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
module Streams
|
|
7
|
+
# The streaming method mixed into a client, which builds a streaming client of the client
|
|
8
|
+
#
|
|
9
|
+
# The x gem includes it into X::Client. With x-core and x-streams alone, include it yourself:
|
|
10
|
+
# X::Client.include(X::Streams::API), or build a streaming client of a client with X::StreamingClient.new. It
|
|
11
|
+
# passes the object it is included into to the streaming client as its client, which is an X::Client, so it belongs
|
|
12
|
+
# in X::Client or a subclass.
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
module API
|
|
16
|
+
# A client for the streaming endpoints, which reads and reconnects differently
|
|
17
|
+
#
|
|
18
|
+
# Each call builds a new streaming client, so the one a stream runs on is kept in a variable to stop it with
|
|
19
|
+
# {StreamingClient#stop}, and a streaming client that was stopped, which stays stopped, is replaced by another.
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
# @param read_timeout [Integer, Float, nil] the timeout for reading from a stream in seconds, as
|
|
23
|
+
# {StreamingClient#initialize} takes it
|
|
24
|
+
# @param max_reconnects [Integer, Float] the maximum number of times in a row to reconnect a stream that drops, as
|
|
25
|
+
# {StreamingClient#initialize} takes it
|
|
26
|
+
# @param on_reconnect [#call, nil] a callable passed the error that dropped a stream and the seconds it waits
|
|
27
|
+
# before each reconnect, as {StreamingClient#initialize} takes it, or nil for none
|
|
28
|
+
# @return [StreamingClient] a streaming client that shares this client's credentials and settings
|
|
29
|
+
# @raise [ArgumentError] if the read timeout is neither a finite number of seconds of at least 25 nor nil, the
|
|
30
|
+
# maximum number of reconnects is neither a count nor Float::INFINITY, or on_reconnect neither responds to call
|
|
31
|
+
# nor is nil
|
|
32
|
+
# @example Stream filtered posts, giving up after five reconnects in a row
|
|
33
|
+
# client.streaming(max_reconnects: 5).stream("tweets/search/stream") { |post| puts post }
|
|
34
|
+
# @example Log each reconnect of a stream
|
|
35
|
+
# client.streaming(on_reconnect: ->(error, wait) { logger.warn("#{error.message}; reconnecting in #{wait}s") })
|
|
36
|
+
def streaming(read_timeout: StreamingClient::DEFAULT_READ_TIMEOUT, max_reconnects: StreamingClient::DEFAULT_MAX_RECONNECTS, on_reconnect: nil)
|
|
37
|
+
StreamingClient.new(_ = self, read_timeout:, max_reconnects:, on_reconnect:)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X
|
|
4
|
+
module Streams
|
|
5
|
+
# Wraps an error a callback of a stream raised, so that it is not taken for an error of the stream
|
|
6
|
+
#
|
|
7
|
+
# A stream runs on_response, the from_response of its object_class, and its block inside the request that reads
|
|
8
|
+
# it, where an IOError or a SystemCallError a callback raised would be taken for a connection that dropped, and an
|
|
9
|
+
# X::ServiceUnavailable for the response of the stream, and the stream reconnected after it. So a callback's error
|
|
10
|
+
# is wrapped in this, which neither X::Client#get_stream nor the reconnects of a stream take for one of theirs, and
|
|
11
|
+
# raised as it was once the stream is left.
|
|
12
|
+
#
|
|
13
|
+
# Internal to x-streams: StreamParser tags the errors of a callback with it, as StreamingClient tags those of the
|
|
14
|
+
# on_response its stream client is passed a failed response with, and ReconnectHandler raises the error it holds
|
|
15
|
+
# in its place.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
class CallbackError < StandardError
|
|
19
|
+
# The error the callback raised
|
|
20
|
+
# @api private
|
|
21
|
+
# @return [StandardError] the error
|
|
22
|
+
attr_reader :error
|
|
23
|
+
|
|
24
|
+
# A callable that tags the error another raises, or nil for no callable
|
|
25
|
+
#
|
|
26
|
+
# X::Client#get_stream tags the error of the on_response it passes a failed response as its own, and raises it
|
|
27
|
+
# as it was, so a callable whose error is not to be taken for one of the stream raises this itself.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
# @param callable [#call, nil] the callable
|
|
31
|
+
# @return [Proc, nil] the callable that tags its errors, or nil if it is nil
|
|
32
|
+
# @example Tag the errors of the on_response of a client
|
|
33
|
+
# X::Streams::CallbackError.tagging(client.on_response)
|
|
34
|
+
def self.tagging(callable)
|
|
35
|
+
callable && lambda do |*arguments|
|
|
36
|
+
callable.call(*arguments)
|
|
37
|
+
rescue => e
|
|
38
|
+
raise new(e)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initialize a new CallbackError
|
|
43
|
+
# @api private
|
|
44
|
+
# @param error [StandardError] the error the callback raised
|
|
45
|
+
# @return [CallbackError] a new instance
|
|
46
|
+
def initialize(error)
|
|
47
|
+
super
|
|
48
|
+
@error = error
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_constant :CallbackError
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "x/core"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
module Streams
|
|
7
|
+
# Base error class for the failures of a stream, which every error x-streams raises of its own descends from
|
|
8
|
+
#
|
|
9
|
+
# It descends from X::Error, so that rescuing the errors of the X API catches the failure of a stream too. The
|
|
10
|
+
# errors that descend from it are named directly under X, as the errors of x-core are, so that this is the one name
|
|
11
|
+
# under X::Streams a rescue reaches for.
|
|
12
|
+
#
|
|
13
|
+
# It catches the errors x-streams raises of its own: a line of a stream that held errors and no data, and rules
|
|
14
|
+
# of the filtered stream the API left unchanged. It does not catch the X::Error of a request the API refused, or
|
|
15
|
+
# that got no response, such as the X::NetworkError of a stream that dropped with no reconnects left, nor the
|
|
16
|
+
# ArgumentError of a mistake in the arguments of a call. Rescue X::Error to catch every failure of a stream.
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
class Error < X::Error
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# The title and detail of each problem, separated by commas
|
|
23
|
+
#
|
|
24
|
+
# It is the message of an error built of problems without a message of its own.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
# @param problems [Array<Problem>] the problems
|
|
28
|
+
# @return [String, nil] the title and detail of each problem, or nil for none
|
|
29
|
+
def describe(problems)
|
|
30
|
+
problems.map { |problem| [problem.title, problem.detail || problem.message].compact.join(": ") }.join(", ") unless problems.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|