jbr 3.5.1 → 3.6.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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +13 -5
- data/lib/graphql/client.rb +13 -1
- data/lib/graphql/throttled.rb +6 -0
- data/lib/jbr/asking.rb +40 -0
- data/lib/jbr/oauth.rb +2 -16
- data/lib/jbr/throttle.rb +8 -0
- data/lib/jbr/version.rb +1 -1
- data/lib/jbr.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8ab569aab529193190b37620954eb2e95db5ac39ccb2a085a7caa2b6cf66981
|
|
4
|
+
data.tar.gz: 4d331b9c225a2d0fb8a978f166985f416528a23eac283c4afb31924aecf7bc8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbe6e4b68d03262584a83279ca00eff9a4a5886413498d5db6cbec643e070d12a863188914e95c87a9365fe8f7aa419796c4238824d3346847fdbbd71960c74e
|
|
7
|
+
data.tar.gz: c0a9624a47f0c4f3706acdabf95bab82cb9d6e2deef035e88edb08daa89d4cdb686bf6489ea6e3c5fcd5b67dcc2cae4388ed19d4f6db036144366a6bcf995864
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [3.6.1] - 2026-08-17
|
|
2
|
+
|
|
3
|
+
- [Fix] 3.6.0 shipped without the two files it added, `graphql/throttled` and `jbr/asking`, so
|
|
4
|
+
requiring the gem raised `LoadError` and nothing worked at all. The gem's file list comes
|
|
5
|
+
from `git ls-files` and the release commit never added them. 3.6.0 is yanked
|
|
6
|
+
|
|
7
|
+
## [3.6.0] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
- [Fix] A refusal for cost the bucket can recover from is waited out and asked again, up to
|
|
10
|
+
four times, rather than raised. A walk of many pages drains the bucket faster than it
|
|
11
|
+
refills and gets `Throttled (cost 1885, 1254 of 10000 available, restoring 500/s)` — 631
|
|
12
|
+
points short of a query the bucket holds five times over. The refusal prices the query, so
|
|
13
|
+
the throttle already knows the shortfall: 1.26 seconds at 500 a second, and the same
|
|
14
|
+
question is answered
|
|
15
|
+
- [Change] Only a query costing more than the bucket *ever* holds is given up on, since
|
|
16
|
+
waiting cannot help it. Where Jobber names no ceiling, one wait is tried rather than the
|
|
17
|
+
worst assumed
|
|
18
|
+
- [New] `GraphQL::Throttled`, a `GraphQL::Error` for a query refused over what it costs rather
|
|
19
|
+
than over anything about the query. It never leaves the gem — a caller still sees
|
|
20
|
+
`Jbr::Error` — but it is what tells the two refusals apart inside it
|
|
21
|
+
- [Change] What credentials do when they ask Jobber something is `Jbr::Asking`, mixed into
|
|
22
|
+
`Jbr::OAuth`, which was over a hundred lines with the retry in it
|
|
23
|
+
|
|
1
24
|
## [3.5.1] - 2026-08-17
|
|
2
25
|
|
|
3
26
|
- [Change] A line item is how many of what, and nothing else. `description` was answered and
|
data/README.md
CHANGED
|
@@ -204,11 +204,19 @@ done about either — every request waits for itself:
|
|
|
204
204
|
A request that follows no other waits for nothing, so a single `find` is as quick as it ever
|
|
205
205
|
was. Only a walk long enough to be a problem is slowed, and only by as much as it must be.
|
|
206
206
|
|
|
207
|
-
Where Jobber refuses
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
Where Jobber refuses anyway, the refusal says what the query would have cost against what was
|
|
208
|
+
available, and what happens next follows from those two numbers:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
Throttled (cost 1885, 1254 of 10000 available, restoring 500/s)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
631 points short of a query the bucket holds five times over, so the shortfall is waited out —
|
|
215
|
+
1.26 seconds at 500 a second — and the same question asked again, up to four times. Only a
|
|
216
|
+
query costing more than the bucket *ever* holds is given up on, since no wait would help it;
|
|
217
|
+
that one arrives as a `Jbr::Error` carrying the line above. Every connection this gem asks for
|
|
218
|
+
is bounded, to keep a query on the affordable side of that: twenty lines to a job, and twenty
|
|
219
|
+
jobs or visits to a page.
|
|
212
220
|
|
|
213
221
|
### Events
|
|
214
222
|
|
data/lib/graphql/client.rb
CHANGED
|
@@ -26,12 +26,24 @@ module GraphQL
|
|
|
26
26
|
# Before the refusal, not after it: an endpoint that reports what it will still answer
|
|
27
27
|
# reports it when it says no, which is when a caller most needs to know.
|
|
28
28
|
yield body['extensions'] if block_given?
|
|
29
|
-
raise
|
|
29
|
+
raise refusal_for(body), refusal(body) if body['errors'].present?
|
|
30
30
|
body.fetch('data')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
# Refused for cost, or refused for the query. An endpoint that priced the query above what
|
|
36
|
+
# was left says so by naming the code, and says it again in the two numbers it reports —
|
|
37
|
+
# either is enough, since only one of them is documented and only one has been seen.
|
|
38
|
+
def refusal_for(body)
|
|
39
|
+
cost = body['extensions'].to_h['cost'].to_h
|
|
40
|
+
available = cost['throttleStatus'].to_h['currentlyAvailable']
|
|
41
|
+
coded = body['errors'].any? { |error| error.to_h.dig('extensions', 'code') == 'THROTTLED' }
|
|
42
|
+
priced = available && cost['requestedQueryCost'].to_f > available.to_f
|
|
43
|
+
|
|
44
|
+
coded || priced ? Throttled : Error
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
# What the endpoint refused, and — where it priced the refusal — what the query would have
|
|
36
48
|
# cost against what was available. `Throttled` on its own leaves a caller unable to tell a
|
|
37
49
|
# query too big to ever run from a bucket that only needed a moment.
|
data/lib/jbr/asking.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Jbr
|
|
2
|
+
# What credentials do when they ask Jobber something: pace the request against both limits
|
|
3
|
+
# Jobber holds an app to, wait out a refusal it can recover from, and refresh a token that
|
|
4
|
+
# went stale on the way.
|
|
5
|
+
module Asking
|
|
6
|
+
# How many times a query refused for cost is asked again. Once is the answer where the
|
|
7
|
+
# bucket only had to refill, since the wait is worked out to cover exactly the shortfall.
|
|
8
|
+
# The rest are for a bucket the whole app shares: another process may drain it again while
|
|
9
|
+
# this one waits, and each attempt costs only the seconds Jobber says it needs.
|
|
10
|
+
ATTEMPTS = 4
|
|
11
|
+
|
|
12
|
+
# Run a statement, waiting for what Jobber will still answer and refreshing a stale token.
|
|
13
|
+
# @return [Hash] the data Jobber answered, or empty when the credentials are dead.
|
|
14
|
+
def query(statement, variables: {})
|
|
15
|
+
attempts = 0
|
|
16
|
+
begin
|
|
17
|
+
throttle.wait
|
|
18
|
+
client.query(statement, variables: variables) { |extensions| throttle.read extensions }
|
|
19
|
+
rescue GraphQL::Unauthorized
|
|
20
|
+
refresh ? retry : {}
|
|
21
|
+
rescue GraphQL::Throttled => error
|
|
22
|
+
# The refusal priced the query, so the throttle now knows the shortfall and `wait` at
|
|
23
|
+
# the top of the retry sits out exactly that. Only a query costing more than the bucket
|
|
24
|
+
# ever holds is hopeless; the rest is a walk that asked a moment too early.
|
|
25
|
+
raise Error, error.message unless throttle.affordable? && (attempts += 1) < ATTEMPTS
|
|
26
|
+
|
|
27
|
+
retry
|
|
28
|
+
rescue GraphQL::Error => error
|
|
29
|
+
# The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
|
|
30
|
+
# was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
|
|
31
|
+
# up instead of hearing that Jobber would not answer.
|
|
32
|
+
raise Error, error.message
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def throttle = @throttle ||= Throttle.new
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/jbr/oauth.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Jbr
|
|
2
2
|
# Credentials for one Jobber account, and the gateway to all they read or write.
|
|
3
3
|
class OAuth
|
|
4
|
+
include Asking
|
|
5
|
+
|
|
4
6
|
# The mutation that revokes the app on the account.
|
|
5
7
|
DISCONNECT_MUTATION = <<~GRAPHQL
|
|
6
8
|
mutation Disconnect {
|
|
@@ -34,20 +36,6 @@ module Jbr
|
|
|
34
36
|
def requests = Request.new oauth: self
|
|
35
37
|
def visits = Visits.new oauth: self
|
|
36
38
|
|
|
37
|
-
# Run a statement, waiting for what Jobber will still answer and refreshing a stale token.
|
|
38
|
-
# @return [Hash] the data Jobber answered, or empty when the credentials are dead.
|
|
39
|
-
def query(statement, variables: {})
|
|
40
|
-
throttle.wait
|
|
41
|
-
client.query(statement, variables: variables) { |extensions| throttle.read extensions }
|
|
42
|
-
rescue GraphQL::Unauthorized
|
|
43
|
-
refresh ? retry : {}
|
|
44
|
-
rescue GraphQL::Error => error
|
|
45
|
-
# The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
|
|
46
|
-
# was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
|
|
47
|
-
# up instead of hearing that Jobber would not answer.
|
|
48
|
-
raise Error, error.message
|
|
49
|
-
end
|
|
50
|
-
|
|
51
39
|
# Delete a token. If the token is invalid, do nothing.
|
|
52
40
|
def delete
|
|
53
41
|
client.query DISCONNECT_MUTATION
|
|
@@ -76,8 +64,6 @@ module Jbr
|
|
|
76
64
|
|
|
77
65
|
private
|
|
78
66
|
|
|
79
|
-
def throttle = @throttle ||= Throttle.new
|
|
80
|
-
|
|
81
67
|
def refresh
|
|
82
68
|
output = self.class.post refresh_token: @refresh_token, grant_type: 'refresh_token'
|
|
83
69
|
@access_token = output[:access_token]
|
data/lib/jbr/throttle.rb
CHANGED
|
@@ -25,9 +25,17 @@ module Jbr
|
|
|
25
25
|
# and only an answered one reports an actual.
|
|
26
26
|
@cost = (cost['actualQueryCost'] || cost['requestedQueryCost']).to_f
|
|
27
27
|
@available = status['currentlyAvailable'].to_f
|
|
28
|
+
@maximum = status['maximumAvailable'].to_f
|
|
28
29
|
@restore_rate = status['restoreRate'].to_f
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Whether the bucket could ever pay for the query it last priced. A refusal is worth
|
|
33
|
+
# waiting out where it could: the shortfall refills and the same query goes through. Where
|
|
34
|
+
# the query costs more than the bucket ever holds, no wait will do — and where Jobber said
|
|
35
|
+
# nothing about the ceiling, one attempt at waiting is cheaper than assuming the worst.
|
|
36
|
+
# @return [Boolean] false only where the ceiling is known and the query is over it.
|
|
37
|
+
def affordable? = !@maximum.to_f.positive? || @cost.to_f <= @maximum
|
|
38
|
+
|
|
31
39
|
private
|
|
32
40
|
|
|
33
41
|
# Nothing is owed to the first caller: a request that follows no other is not too soon.
|
data/lib/jbr/version.rb
CHANGED
data/lib/jbr.rb
CHANGED
|
@@ -11,6 +11,7 @@ require 'active_support/core_ext/array/conversions'
|
|
|
11
11
|
|
|
12
12
|
require 'graphql/error'
|
|
13
13
|
require 'graphql/unauthorized'
|
|
14
|
+
require 'graphql/throttled'
|
|
14
15
|
require 'graphql/client'
|
|
15
16
|
|
|
16
17
|
require 'jbr/mock'
|
|
@@ -26,6 +27,7 @@ require 'jbr/cliental'
|
|
|
26
27
|
require 'jbr/named'
|
|
27
28
|
require 'jbr/resource'
|
|
28
29
|
require 'jbr/throttle'
|
|
30
|
+
require 'jbr/asking'
|
|
29
31
|
require 'jbr/request'
|
|
30
32
|
require 'jbr/oauth'
|
|
31
33
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jbr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -106,9 +106,11 @@ files:
|
|
|
106
106
|
- README.md
|
|
107
107
|
- lib/graphql/client.rb
|
|
108
108
|
- lib/graphql/error.rb
|
|
109
|
+
- lib/graphql/throttled.rb
|
|
109
110
|
- lib/graphql/unauthorized.rb
|
|
110
111
|
- lib/jbr.rb
|
|
111
112
|
- lib/jbr/account.rb
|
|
113
|
+
- lib/jbr/asking.rb
|
|
112
114
|
- lib/jbr/client.rb
|
|
113
115
|
- lib/jbr/cliental.rb
|
|
114
116
|
- lib/jbr/error.rb
|