async-background 1.0.0 → 1.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 +4 -4
- data/CHANGELOG.md +319 -276
- data/README.md +91 -109
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +39 -18
- data/lib/async/background/web/auth.rb +7 -2
- data/lib/async/background/web/configuration.rb +17 -3
- data/lib/async/background/web/event_hub.rb +25 -148
- data/lib/async/background/web/response.rb +31 -9
- data/lib/async/background/web/router.rb +3 -1
- data/lib/async/background/web/stream.rb +54 -15
- metadata +2 -2
|
@@ -15,9 +15,27 @@ module Async
|
|
|
15
15
|
CSS_TYPE = 'text/css; charset=utf-8'
|
|
16
16
|
NO_STORE = 'no-store'
|
|
17
17
|
ASSET_CACHE = 'public, max-age=31536000, immutable'
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
BASE_SECURITY_HEADERS = {
|
|
19
|
+
'x-content-type-options' => 'nosniff',
|
|
20
|
+
'referrer-policy' => 'no-referrer',
|
|
21
|
+
'cross-origin-resource-policy' => 'same-origin'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
HTML_SECURITY_HEADERS = BASE_SECURITY_HEADERS.merge(
|
|
25
|
+
'x-frame-options' => 'DENY',
|
|
26
|
+
'content-security-policy' =>
|
|
27
|
+
"default-src 'none'; " \
|
|
28
|
+
"script-src 'self'; " \
|
|
29
|
+
"style-src 'self'; " \
|
|
30
|
+
"img-src 'self' data:; " \
|
|
31
|
+
"connect-src 'self'; " \
|
|
32
|
+
"frame-ancestors 'none'; " \
|
|
33
|
+
"base-uri 'none'; " \
|
|
34
|
+
"form-action 'none'"
|
|
35
|
+
).freeze
|
|
36
|
+
|
|
37
|
+
UNAUTHORIZED_BODY = JSON.generate(error: 'unauthorized').freeze
|
|
38
|
+
NOT_FOUND_BODY = JSON.generate(error: 'not_found').freeze
|
|
21
39
|
BAD_REQUEST_BODY = JSON.generate(error: 'invalid_request').freeze
|
|
22
40
|
UNAVAILABLE_BODY = JSON.generate(error: 'service_unavailable').freeze
|
|
23
41
|
INTERNAL_ERROR_BODY = JSON.generate(error: 'internal_error').freeze
|
|
@@ -32,7 +50,7 @@ module Async
|
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
def html(body)
|
|
35
|
-
[200,
|
|
53
|
+
[200, html_headers, [body]]
|
|
36
54
|
end
|
|
37
55
|
|
|
38
56
|
def javascript(body)
|
|
@@ -44,11 +62,11 @@ module Async
|
|
|
44
62
|
end
|
|
45
63
|
|
|
46
64
|
def unauthorized
|
|
47
|
-
[401, no_store_headers(
|
|
65
|
+
[401, no_store_headers(JSON_TYPE), [UNAUTHORIZED_BODY]]
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
def not_found
|
|
51
|
-
[404, no_store_headers(
|
|
69
|
+
[404, no_store_headers(JSON_TYPE), [NOT_FOUND_BODY]]
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
def bad_request(message = nil)
|
|
@@ -65,11 +83,15 @@ module Async
|
|
|
65
83
|
end
|
|
66
84
|
|
|
67
85
|
def no_store_headers(content_type)
|
|
68
|
-
{'content-type' => content_type, 'cache-control' => NO_STORE}
|
|
86
|
+
{'content-type' => content_type, 'cache-control' => NO_STORE}.merge(BASE_SECURITY_HEADERS)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def html_headers
|
|
90
|
+
{'content-type' => HTML_TYPE, 'cache-control' => NO_STORE}.merge(HTML_SECURITY_HEADERS)
|
|
69
91
|
end
|
|
70
92
|
|
|
71
93
|
def asset_headers(content_type)
|
|
72
|
-
{'content-type' => content_type, 'cache-control' => ASSET_CACHE}
|
|
94
|
+
{'content-type' => content_type, 'cache-control' => ASSET_CACHE}.merge(BASE_SECURITY_HEADERS)
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def sse_headers
|
|
@@ -77,7 +99,7 @@ module Async
|
|
|
77
99
|
'content-type' => EVENT_STREAM_TYPE,
|
|
78
100
|
'cache-control' => 'no-cache, no-transform',
|
|
79
101
|
'x-accel-buffering' => 'no'
|
|
80
|
-
}
|
|
102
|
+
}.merge(BASE_SECURITY_HEADERS)
|
|
81
103
|
end
|
|
82
104
|
end
|
|
83
105
|
end
|
|
@@ -19,8 +19,10 @@ module Async
|
|
|
19
19
|
'/api/stream' => :stream
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
|
+
ALLOWED_METHODS = %w[GET HEAD].freeze
|
|
23
|
+
|
|
22
24
|
def match(env)
|
|
23
|
-
return unless env['REQUEST_METHOD']
|
|
25
|
+
return unless ALLOWED_METHODS.include?(env['REQUEST_METHOD'])
|
|
24
26
|
|
|
25
27
|
GET_ROUTES[env['PATH_INFO'] || '/']
|
|
26
28
|
end
|
|
@@ -1,41 +1,80 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../clock'
|
|
4
|
+
|
|
3
5
|
module Async
|
|
4
6
|
module Background
|
|
5
7
|
module Web
|
|
6
8
|
class Stream
|
|
7
|
-
|
|
9
|
+
include Clock
|
|
10
|
+
|
|
11
|
+
def initialize(hub, heartbeat_seconds:, retry_ms:, poll_seconds:, logger: nil)
|
|
8
12
|
@hub = hub
|
|
9
13
|
@heartbeat_seconds = heartbeat_seconds
|
|
10
14
|
@retry_ms = retry_ms
|
|
15
|
+
@poll_seconds = poll_seconds
|
|
16
|
+
@logger = logger
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def each
|
|
14
|
-
subscription, initial_frame = @hub.subscribe
|
|
15
20
|
yield "retry: #{@retry_ms}\n\n"
|
|
16
|
-
|
|
21
|
+
|
|
22
|
+
version, frame = initial_state
|
|
23
|
+
if version.nil?
|
|
24
|
+
yield EventHub::UNAVAILABLE_FRAME
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
yield frame
|
|
29
|
+
last_yield = monotonic_now
|
|
30
|
+
unavailable_announced = false
|
|
17
31
|
|
|
18
32
|
loop do
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
sleep_for_poll
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
new_version = @hub.current_version
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
if new_version != version
|
|
39
|
+
version = new_version
|
|
40
|
+
yield @hub.frame_for(version)
|
|
41
|
+
last_yield = monotonic_now
|
|
42
|
+
elsif (monotonic_now - last_yield) >= @heartbeat_seconds
|
|
43
|
+
yield EventHub::HEARTBEAT_FRAME
|
|
44
|
+
last_yield = monotonic_now
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unavailable_announced = false
|
|
48
|
+
rescue ClosedError
|
|
49
|
+
break
|
|
50
|
+
rescue UnavailableError
|
|
51
|
+
unless unavailable_announced
|
|
52
|
+
yield EventHub::UNAVAILABLE_FRAME
|
|
53
|
+
unavailable_announced = true
|
|
54
|
+
last_yield = monotonic_now
|
|
55
|
+
end
|
|
56
|
+
end
|
|
23
57
|
end
|
|
24
|
-
rescue Errno::EPIPE, IOError
|
|
58
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
25
59
|
nil
|
|
26
|
-
rescue
|
|
27
|
-
|
|
60
|
+
rescue StandardError => error
|
|
61
|
+
@logger&.error(
|
|
62
|
+
"[async-background-web] SSE stream terminated: " \
|
|
63
|
+
"#{error.class}: #{error.message}"
|
|
64
|
+
)
|
|
28
65
|
nil
|
|
29
|
-
ensure
|
|
30
|
-
@hub.unsubscribe(subscription) if subscription
|
|
31
66
|
end
|
|
32
67
|
|
|
33
68
|
private
|
|
34
69
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
rescue
|
|
38
|
-
nil
|
|
70
|
+
def initial_state
|
|
71
|
+
@hub.initial_frame
|
|
72
|
+
rescue ClosedError, UnavailableError
|
|
73
|
+
[nil, nil]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def sleep_for_poll
|
|
77
|
+
sleep(@poll_seconds)
|
|
39
78
|
end
|
|
40
79
|
end
|
|
41
80
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-background
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|