sferik 0.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/.yardopts +5 -0
- data/CHANGELOG.md +72 -0
- data/LICENSE.md +16 -0
- data/README.md +517 -0
- data/exe/sferik +6 -0
- data/lib/sferik/api/code_endpoints.rb +37 -0
- data/lib/sferik/api/profile_endpoints.rb +113 -0
- data/lib/sferik/api/resume_endpoints.rb +41 -0
- data/lib/sferik/api/site_endpoints.rb +247 -0
- data/lib/sferik/api/talk_endpoints.rb +45 -0
- data/lib/sferik/api.rb +23 -0
- data/lib/sferik/block.rb +25 -0
- data/lib/sferik/body.rb +54 -0
- data/lib/sferik/cache.rb +361 -0
- data/lib/sferik/cli.rb +369 -0
- data/lib/sferik/client.rb +453 -0
- data/lib/sferik/collection.rb +82 -0
- data/lib/sferik/configuration.rb +129 -0
- data/lib/sferik/connections.rb +151 -0
- data/lib/sferik/contributions.rb +86 -0
- data/lib/sferik/day.rb +35 -0
- data/lib/sferik/dependency.rb +26 -0
- data/lib/sferik/deployment.rb +36 -0
- data/lib/sferik/errors.rb +234 -0
- data/lib/sferik/figure.rb +83 -0
- data/lib/sferik/finger.rb +74 -0
- data/lib/sferik/freshness.rb +54 -0
- data/lib/sferik/home/pages.rb +27 -0
- data/lib/sferik/home/profile.rb +51 -0
- data/lib/sferik/home/section.rb +27 -0
- data/lib/sferik/home.rb +38 -0
- data/lib/sferik/json_parsing.rb +28 -0
- data/lib/sferik/name_change.rb +49 -0
- data/lib/sferik/place.rb +33 -0
- data/lib/sferik/podcast.rb +41 -0
- data/lib/sferik/project.rb +49 -0
- data/lib/sferik/projects.rb +138 -0
- data/lib/sferik/push.rb +33 -0
- data/lib/sferik/resource.rb +373 -0
- data/lib/sferik/resume/award.rb +43 -0
- data/lib/sferik/resume/basics.rb +77 -0
- data/lib/sferik/resume/education.rb +59 -0
- data/lib/sferik/resume/location.rb +35 -0
- data/lib/sferik/resume/meta.rb +35 -0
- data/lib/sferik/resume/patent.rb +43 -0
- data/lib/sferik/resume/profile.rb +35 -0
- data/lib/sferik/resume/project.rb +27 -0
- data/lib/sferik/resume/skill.rb +27 -0
- data/lib/sferik/resume/speaking.rb +19 -0
- data/lib/sferik/resume/volunteer.rb +59 -0
- data/lib/sferik/resume/work.rb +51 -0
- data/lib/sferik/resume.rb +125 -0
- data/lib/sferik/session.rb +41 -0
- data/lib/sferik/social_profile.rb +49 -0
- data/lib/sferik/status/github.rb +38 -0
- data/lib/sferik/status/loaded.rb +48 -0
- data/lib/sferik/status.rb +33 -0
- data/lib/sferik/talk.rb +73 -0
- data/lib/sferik/talks.rb +79 -0
- data/lib/sferik/validation.rb +119 -0
- data/lib/sferik/version.rb +7 -0
- data/lib/sferik/web_finger/link.rb +46 -0
- data/lib/sferik/web_finger.rb +43 -0
- data/lib/sferik/who.rb +53 -0
- data/lib/sferik/whoami.rb +36 -0
- data/lib/sferik/wrapping.rb +114 -0
- data/lib/sferik.rb +246 -0
- data/sig/manifest.yaml +13 -0
- data/sig/sferik.rbs +825 -0
- metadata +118 -0
data/lib/sferik/cache.rb
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "freshness"
|
|
5
|
+
|
|
6
|
+
module Sferik
|
|
7
|
+
# Keeps the responses to a client's GET requests, and asks again only for what may have changed
|
|
8
|
+
#
|
|
9
|
+
# It stands where the client's {Connections} do, and makes its requests over them. A response says how long it's
|
|
10
|
+
# good for (Cache-Control: max-age), and for that long the same URL asked for as the same media type is answered
|
|
11
|
+
# with it, and no request is made. After that, the request says which version is kept (If-None-Match, with the
|
|
12
|
+
# response's ETag), and the server answers 304 Not Modified, without a body, if that's still the one. A response
|
|
13
|
+
# that says not to keep it (no-store) isn't kept, and one that says to check each time (no-cache) is checked each
|
|
14
|
+
# time. Only a 200 is kept, and what's kept stays kept when the server answers with an error of its own (a 5xx),
|
|
15
|
+
# which says nothing of whether it has changed: the next request asks again.
|
|
16
|
+
#
|
|
17
|
+
# A response that has already been kept somewhere on its way, as one from Cloudflare's cache has, says for how long
|
|
18
|
+
# (Age), and is good for that much less. That cache answers with what it has, when that's no longer good, while it
|
|
19
|
+
# builds another for whoever asks next. So each request this cache makes says not to be answered that way
|
|
20
|
+
# (Cache-Control: no-cache), which the site takes to mean that it should wait for the new one: what's still good
|
|
21
|
+
# there is its answer all the same. And a cache that's told to (stale) answers with a response that's no longer
|
|
22
|
+
# good, when the server can't be asked whether it has changed, or answers with an error of its own.
|
|
23
|
+
#
|
|
24
|
+
# Threads that ask for the same thing at once, when it isn't kept or is no longer good, make one request between
|
|
25
|
+
# them: the first asks, and the rest wait for its answer, which is theirs too. If it gets none, each asks for itself.
|
|
26
|
+
#
|
|
27
|
+
# A hundred responses are kept at most: one more, and the one that was asked of the server longest ago is forgotten.
|
|
28
|
+
#
|
|
29
|
+
# What a client makes of a response (the JSON parsed, and a resource built of it) is kept with the response, and
|
|
30
|
+
# made once: see {#made}.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
class Cache
|
|
34
|
+
# A response that's kept, with its ETag, if it has one, when on the clock it's good until, and what was made of it
|
|
35
|
+
#
|
|
36
|
+
# @!attribute [r] response
|
|
37
|
+
# The response
|
|
38
|
+
# @api private
|
|
39
|
+
# @return [Net::HTTPResponse] the response
|
|
40
|
+
# @!attribute [r] etag
|
|
41
|
+
# The response's ETag
|
|
42
|
+
# @api private
|
|
43
|
+
# @return [String, nil] the ETag, or nil if the response has none
|
|
44
|
+
# @!attribute [r] expires
|
|
45
|
+
# When the response is good until
|
|
46
|
+
# @api private
|
|
47
|
+
# @return [Numeric] the time on the cache's clock, in seconds
|
|
48
|
+
# @!attribute [r] made
|
|
49
|
+
# What was made of the response
|
|
50
|
+
# @api private
|
|
51
|
+
# @return [Object, nil] what {Cache#made}'s block returned, or nil if nothing has been made of it yet
|
|
52
|
+
Entry = Data.define(:response, :etag, :expires, :made)
|
|
53
|
+
private_constant :Entry
|
|
54
|
+
|
|
55
|
+
# A request on its way, whose answer is for every thread that asks for the same thing before it comes
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
class Flight
|
|
59
|
+
# Initialize a flight, which hasn't landed
|
|
60
|
+
#
|
|
61
|
+
# @api private
|
|
62
|
+
# @return [Flight] the flight
|
|
63
|
+
def initialize
|
|
64
|
+
@landed = Queue.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Make the request, and say what it got
|
|
68
|
+
#
|
|
69
|
+
# That's said to the threads that are waiting, and to any that ask later.
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
# @yield the request to make
|
|
73
|
+
# @yieldreturn [Net::HTTPResponse] the response
|
|
74
|
+
# @return [Net::HTTPResponse] the response
|
|
75
|
+
def fly
|
|
76
|
+
@response = yield
|
|
77
|
+
ensure
|
|
78
|
+
@landed.close
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Wait for the request to get its answer, and return it
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
84
|
+
# @return [Net::HTTPResponse, nil] the response, or nil if the request got none
|
|
85
|
+
def response
|
|
86
|
+
@landed.deq
|
|
87
|
+
@response
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
private_constant :Flight
|
|
91
|
+
|
|
92
|
+
# The time, in seconds, on a clock that only goes forward
|
|
93
|
+
CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
94
|
+
private_constant :CLOCK
|
|
95
|
+
|
|
96
|
+
# The most responses to keep: far more than the API has, but an end to what a client keeps of URLs that are made
|
|
97
|
+
# up as it goes, each with a query of its own
|
|
98
|
+
LIMIT = 100
|
|
99
|
+
private_constant :LIMIT
|
|
100
|
+
|
|
101
|
+
# Initialize a cache
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
# @param connections [Connections, Cache] what to make requests over
|
|
105
|
+
# @param clock [#call] what tells the time, in seconds
|
|
106
|
+
# @param entries [Hash{Array => Entry}] the responses kept, by URL and media type
|
|
107
|
+
# @param lock [Mutex] what's held to read or change them, and the requests on their way
|
|
108
|
+
# @param stale [Boolean] whether to answer with a response that's no longer good, when the server can't be reached,
|
|
109
|
+
# or answers with an error of its own
|
|
110
|
+
# @return [Cache] the cache
|
|
111
|
+
def initialize(connections, clock = CLOCK, entries = {}, lock = Mutex.new, stale: false)
|
|
112
|
+
@connections = connections
|
|
113
|
+
@clock = clock
|
|
114
|
+
@entries = entries
|
|
115
|
+
@lock = lock
|
|
116
|
+
@flights = {} #: Hash[key, Flight]
|
|
117
|
+
@stale = stale
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Yield a cache of the same responses over connections that are kept open
|
|
121
|
+
#
|
|
122
|
+
# It's this cache in all but its connections: what one keeps, the other has, and a request of one's that's on its
|
|
123
|
+
# way is one the other waits for.
|
|
124
|
+
#
|
|
125
|
+
# @api private
|
|
126
|
+
# @yield [cache] the requests to make
|
|
127
|
+
# @yieldparam cache [Cache] a cache whose connections are kept open
|
|
128
|
+
# @yieldreturn [Object] anything
|
|
129
|
+
# @return [Object] what the block returns
|
|
130
|
+
def keeping
|
|
131
|
+
@connections.keeping { |kept| yield dup.over(kept) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Close the connections requests are made over, and keep the responses
|
|
135
|
+
#
|
|
136
|
+
# @api private
|
|
137
|
+
# @return [nil]
|
|
138
|
+
def close = @connections.close
|
|
139
|
+
|
|
140
|
+
# The connections requests are made over, which keep no responses
|
|
141
|
+
#
|
|
142
|
+
# They're what another cache is built over, so that it isn't over this one.
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
145
|
+
# @return [Connections] the connections
|
|
146
|
+
def uncached = @connections
|
|
147
|
+
|
|
148
|
+
# Send a request, unless it's a GET whose response is kept and still good
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
# @param request [Net::HTTPRequest] the request, to a URL
|
|
152
|
+
# @return [Net::HTTPResponse] the response, which may be one that was kept, or the server's error if the cache isn't
|
|
153
|
+
# to answer with what's kept instead
|
|
154
|
+
# @raise [NetworkError] if the server can't be reached, or its response can't be read, and there's no response
|
|
155
|
+
# kept to answer with instead, or the cache isn't to
|
|
156
|
+
def request(request)
|
|
157
|
+
return @connections.request(request) unless request.instance_of?(Net::HTTP::Get)
|
|
158
|
+
|
|
159
|
+
key = [request.uri, request.fetch("accept")] #: key
|
|
160
|
+
entry = @lock.synchronize { @entries[key] }
|
|
161
|
+
(entry && @clock.call < entry.expires) ? entry.response : share(key) { renew(key, entry, request) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# What a block makes of a response, which is made once of a response that's kept
|
|
165
|
+
#
|
|
166
|
+
# The cache answers with a response it keeps again and again, and what's made of it would be the same each time:
|
|
167
|
+
# so that's kept with it, and the block isn't called again until another response is. It should make something
|
|
168
|
+
# that can't be changed, since every caller gets the same one.
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @param uri [URI::HTTP] the URL that was asked for
|
|
172
|
+
# @param accept [String] the media type it was asked for as
|
|
173
|
+
# @param response [Net::HTTPResponse] the response that came
|
|
174
|
+
# @yield what to make of the response
|
|
175
|
+
# @yieldreturn [Object] what's made of it, which isn't nil or false
|
|
176
|
+
# @return [Object] what the block returned: this time, or the first time for this response
|
|
177
|
+
def made(uri, accept, response)
|
|
178
|
+
key = [uri, accept] #: key
|
|
179
|
+
entry = @lock.synchronize { @entries[key] }
|
|
180
|
+
return yield unless entry && entry.response.equal?(response)
|
|
181
|
+
|
|
182
|
+
entry.made || remember(key, entry, yield)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
protected
|
|
186
|
+
|
|
187
|
+
# Make this cache's requests over other connections
|
|
188
|
+
#
|
|
189
|
+
# @api private
|
|
190
|
+
# @param connections [Connections, Cache] the connections
|
|
191
|
+
# @return [Cache] the cache itself
|
|
192
|
+
def over(connections)
|
|
193
|
+
@connections = connections
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# Make a request, unless one for the same thing is on its way
|
|
200
|
+
#
|
|
201
|
+
# Then wait for that one, and answer with what it gets. If it gets no answer, the request is made after all.
|
|
202
|
+
#
|
|
203
|
+
# @api private
|
|
204
|
+
# @param key [Array] the URL and the media type asked for
|
|
205
|
+
# @yield the request to make
|
|
206
|
+
# @yieldreturn [Net::HTTPResponse] the response
|
|
207
|
+
# @return [Net::HTTPResponse] the response: this request's, or the one that was on its way
|
|
208
|
+
def share(key, &ask)
|
|
209
|
+
mine = Flight.new
|
|
210
|
+
flight = @lock.synchronize { @flights[key] ||= mine }
|
|
211
|
+
return flight.response || ask.call unless flight.equal?(mine)
|
|
212
|
+
|
|
213
|
+
lead(key, mine, &ask)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Make a request that other threads may be waiting for, and tell them what it gets
|
|
217
|
+
#
|
|
218
|
+
# It's no longer on its way before they're told, so that a thread that asks after it has landed starts another.
|
|
219
|
+
#
|
|
220
|
+
# @api private
|
|
221
|
+
# @param key [Array] the URL and the media type asked for
|
|
222
|
+
# @param flight [Flight] the request on its way
|
|
223
|
+
# @yield the request to make
|
|
224
|
+
# @yieldreturn [Net::HTTPResponse] the response
|
|
225
|
+
# @return [Net::HTTPResponse] the response
|
|
226
|
+
def lead(key, flight, &ask)
|
|
227
|
+
flight.fly do
|
|
228
|
+
ask.call
|
|
229
|
+
ensure
|
|
230
|
+
@lock.synchronize { @flights.delete(key) }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Send a GET request, or answer with what's kept if the server can't be reached
|
|
235
|
+
#
|
|
236
|
+
# What comes back is kept. What's kept and no longer good is the answer only for a cache that's told to (stale).
|
|
237
|
+
#
|
|
238
|
+
# @api private
|
|
239
|
+
# @param key [Array] the URL and the media type asked for
|
|
240
|
+
# @param entry [Entry, nil] the response kept, which is no longer good, if there is one
|
|
241
|
+
# @param request [Net::HTTP::Get] the request
|
|
242
|
+
# @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed, or can't be reached
|
|
243
|
+
# and the cache answers with what's no longer good
|
|
244
|
+
# @raise [NetworkError] if the server can't be reached, or its response can't be read, and there's no response
|
|
245
|
+
# kept to answer with instead, or the cache isn't to
|
|
246
|
+
def renew(key, entry, request)
|
|
247
|
+
ask(key, entry, request)
|
|
248
|
+
rescue NetworkError
|
|
249
|
+
raise unless entry && @stale
|
|
250
|
+
|
|
251
|
+
entry.response
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Send a GET request, and keep what comes back
|
|
255
|
+
#
|
|
256
|
+
# The request says which version of its response is kept, so that the server sends a body only for another. And
|
|
257
|
+
# it says not to be answered from a cache (Cache-Control: no-cache): the one in front of the site answers with
|
|
258
|
+
# what it has kept past what it's good for, otherwise, which would be no longer good when it came, and to be
|
|
259
|
+
# asked for again. What that cache has that's still good is its answer either way. An error of the server's own
|
|
260
|
+
# (a 5xx) leaves what's kept as it is, to be asked after again.
|
|
261
|
+
#
|
|
262
|
+
# @api private
|
|
263
|
+
# @param key [Array] the URL and the media type asked for
|
|
264
|
+
# @param entry [Entry, nil] the response kept, which is no longer good, if there is one
|
|
265
|
+
# @param request [Net::HTTP::Get] the request
|
|
266
|
+
# @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed, or answers with
|
|
267
|
+
# an error of its own and the cache answers with what's no longer good
|
|
268
|
+
def ask(key, entry, request)
|
|
269
|
+
request["if-none-match"] = entry.etag if entry # no header, for a response that has no ETag
|
|
270
|
+
request["cache-control"] = "no-cache"
|
|
271
|
+
response = @connections.request(request)
|
|
272
|
+
return spared(entry, response) if entry && response.is_a?(Net::HTTPServerError)
|
|
273
|
+
|
|
274
|
+
renewed(key, entry, response)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Keep what the server answers with, or go on keeping what it says hasn't changed
|
|
278
|
+
#
|
|
279
|
+
# @api private
|
|
280
|
+
# @param key [Array] the URL and the media type asked for
|
|
281
|
+
# @param entry [Entry, nil] the response kept, which is no longer good, if there is one
|
|
282
|
+
# @param response [Net::HTTPResponse] the server's answer
|
|
283
|
+
# @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed
|
|
284
|
+
def renewed(key, entry, response)
|
|
285
|
+
kept = entry if response.instance_of?(Net::HTTPNotModified)
|
|
286
|
+
latest = kept ? kept.with(expires: expiry(response)) : Entry.new(response, response["etag"], expiry(response), nil)
|
|
287
|
+
keep(key, latest, response)
|
|
288
|
+
latest.response
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Keep what was made of a response with it, if it's still the one that's kept
|
|
292
|
+
#
|
|
293
|
+
# @api private
|
|
294
|
+
# @param key [Array] the URL and the media type asked for
|
|
295
|
+
# @param entry [Entry] the response kept, when it was made
|
|
296
|
+
# @param made [Object] what was made of it
|
|
297
|
+
# @return [Object] what was made of it
|
|
298
|
+
def remember(key, entry, made)
|
|
299
|
+
@lock.synchronize { @entries[key] = entry.with(made:) if @entries[key].equal?(entry) }
|
|
300
|
+
made
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# What to answer with when the server fails, and a response is kept
|
|
304
|
+
#
|
|
305
|
+
# The server fails when it answers with an error of its own (a 5xx).
|
|
306
|
+
#
|
|
307
|
+
# @api private
|
|
308
|
+
# @param entry [Entry] the response kept, which is no longer good
|
|
309
|
+
# @param response [Net::HTTPResponse] the server's error
|
|
310
|
+
# @return [Net::HTTPResponse] the response kept, for a cache that answers with what's no longer good (stale), or
|
|
311
|
+
# else the error
|
|
312
|
+
def spared(entry, response)
|
|
313
|
+
@stale ? entry.response : response
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Keep a response, or forget the one kept
|
|
317
|
+
#
|
|
318
|
+
# Only a 200 is kept, and not one that says not to keep it: anything else leaves nothing kept. (An error of the
|
|
319
|
+
# server's own, with a response kept, never gets here.) One that's kept is the latest, whether or not one was
|
|
320
|
+
# kept for the same thing before.
|
|
321
|
+
#
|
|
322
|
+
# @api private
|
|
323
|
+
# @param key [Array] the URL and the media type asked for
|
|
324
|
+
# @param entry [Entry] the response to keep, with when it's good until
|
|
325
|
+
# @param answer [Net::HTTPResponse] the latest answer: the response itself, or a 304 that says it hasn't changed
|
|
326
|
+
# @return [void]
|
|
327
|
+
def keep(key, entry, answer)
|
|
328
|
+
keepable = entry.response.instance_of?(Net::HTTPOK) && Freshness.keepable?(answer)
|
|
329
|
+
@lock.synchronize do
|
|
330
|
+
@entries.delete(key)
|
|
331
|
+
hold(key, entry) if keepable
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Keep a response as the latest, and no more than the most there may be
|
|
336
|
+
#
|
|
337
|
+
# One too many, and the response kept longest ago is forgotten. The lock is held by whatever calls this.
|
|
338
|
+
#
|
|
339
|
+
# @api private
|
|
340
|
+
# @param key [Array] the URL and the media type asked for, which nothing is kept for
|
|
341
|
+
# @param entry [Entry] the response to keep
|
|
342
|
+
# @return [void]
|
|
343
|
+
def hold(key, entry)
|
|
344
|
+
@entries[key] = entry
|
|
345
|
+
@entries.shift if @entries.size > LIMIT
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# When a response is good until
|
|
349
|
+
#
|
|
350
|
+
# That's for as long as the latest answer says it's good for, less how long that answer says it has been kept
|
|
351
|
+
# already (Age).
|
|
352
|
+
#
|
|
353
|
+
# @api private
|
|
354
|
+
# @param latest [Net::HTTPResponse] the latest answer: the response itself, or a 304 that says it hasn't changed
|
|
355
|
+
# @return [Numeric] the time on the cache's clock, in seconds
|
|
356
|
+
def expiry(latest)
|
|
357
|
+
@clock.call + Freshness.left(latest)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
private_constant :Cache
|
|
361
|
+
end
|