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.
@@ -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
- UNAUTHORIZED_BODY = 'unauthorized'
20
- NOT_FOUND_BODY = 'not found'
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, no_store_headers(HTML_TYPE), [body]]
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(TEXT_TYPE), [UNAUTHORIZED_BODY]]
65
+ [401, no_store_headers(JSON_TYPE), [UNAUTHORIZED_BODY]]
48
66
  end
49
67
 
50
68
  def not_found
51
- [404, no_store_headers(TEXT_TYPE), [NOT_FOUND_BODY]]
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'] == 'GET'
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
- def initialize(hub, heartbeat_seconds:, retry_ms:)
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
- yield initial_frame
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
- frame = subscription.pop(timeout: @heartbeat_seconds)
20
- break if frame.nil? && subscription.closed?
33
+ sleep_for_poll
34
+
35
+ begin
36
+ new_version = @hub.current_version
21
37
 
22
- yield(frame || EventHub::HEARTBEAT_FRAME)
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 ClosedError, UnavailableError
27
- safe_yield(EventHub::UNAVAILABLE_FRAME) { |frame| yield frame }
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 safe_yield(frame)
36
- yield frame
37
- rescue Errno::EPIPE, IOError
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.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-28 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async