active_storage-async_variants 0.8.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +5 -48
- data/Rakefile +1 -20
- data/config/routes.rb +0 -17
- data/gemfiles/rails_7.2.gemfile +0 -4
- data/gemfiles/rails_8.0.gemfile +0 -4
- data/gemfiles/rails_8.1.gemfile +0 -4
- data/lib/active_storage/async_variants/version.rb +1 -1
- data/lib/active_storage/async_variants.rb +3 -35
- metadata +2 -47
- data/app/assets/javascripts/progress-bar.js +0 -347
- data/app/assets/javascripts/retry.js +0 -11
- data/app/assets/stylesheets/progress.css +0 -35
- data/app/assets/stylesheets/retry.css +0 -98
- data/app/controllers/active_storage/async_variants/states_controller.rb +0 -71
- data/app/views/active_storage/async_variants/states/_failed.html.erb +0 -44
- data/app/views/active_storage/async_variants/states/_pending.html.erb +0 -1
- data/app/views/active_storage/async_variants/states/_processed.html.erb +0 -5
- data/app/views/active_storage/async_variants/states/_processing.html.erb +0 -15
- data/app/views/active_storage/async_variants/states/show.html.erb +0 -10
- data/features/retry_flow.feature +0 -20
- data/features/retry_visibility.feature +0 -23
- data/features/state_rendering.feature +0 -40
- data/features/step_definitions/dummy_steps.rb +0 -144
- data/features/support/env.rb +0 -28
- data/lib/active_storage/async_variants/asset_tag_helper_extension.rb +0 -59
- data/lib/active_storage/async_variants/helper.rb +0 -80
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d60acebdeb8fe9482d73576e79abb4737fb052e6df939e09829b9c0e766b9e12
|
|
4
|
+
data.tar.gz: 68be3b6ef1f9cb34e5a08f87b20470520367e16fdcc81ec73f18d5956ab8979a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3fc174236833deeda757d08b0f8ff68769d5130beb7b3f20fd68082e73aa72ac2a088fd23fad45209cd9849caa9d0293ec5f9f11bd365dec3e6d995a5b79fb0
|
|
7
|
+
data.tar.gz: 7944a7832431949722581b13f62c99b785df7abfaf7693ea236213df1a08e730d9070bce1465ea32255b29423dc393d30bfd61f9b72cf2bdc39b48010bcd806d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.9.0]
|
|
2
|
+
|
|
3
|
+
- **Breaking:** Split the opinionated UI out into a separate gem, [`active_storage-async_variants-ui`](https://github.com/botandrose/active_storage-async_variants-ui). This gem is now UI-free plumbing — the async state machine, `ProcessJob`, the heartbeat watchdog, the external-service callback endpoint, and the `Transformer` base class. Its only runtime dependency is `activestorage` (no more `turbo-rails` / `isolate_assets`).
|
|
4
|
+
- Moved to the UI gem: `image_tag`/`video_tag` with `async:`/`direct:`, the `<turbo-frame>` state endpoint and partials, the circular progress bar, the filename-bearing placeholder route, the retry affordance, and the `cdn_host` / `parent_controller` / `retry_visible_if` configuration. A variant's `.url` (serving the original until processed) is unchanged.
|
|
5
|
+
- Migration note: apps that relied on the rendering helpers should add `gem "active_storage-async_variants-ui"`. Apps using `active_storage-crucible` or only the variant pipeline need no change.
|
|
6
|
+
|
|
1
7
|
## [0.8.0]
|
|
2
8
|
|
|
3
9
|
- The processing progress bar now advances smoothly between polls: variants expose a `rate` (percent-per-second, derived from the record's `created_at`, last heartbeat, and reported progress) that the `@botandrose/progress-bar` element uses to optimistically creep forward, instead of only stepping on each refresh. Includes a migration adding a `created_at` column to `active_storage_variant_records`; run it before deploying.
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Extends Active Storage with pluggable per-variant transformers, async-safe variant processing, and failure handling.
|
|
4
4
|
|
|
5
|
+
This gem is the UI-free plumbing. A variant's `.url` serves the original while it is pending/processing/failed, and the processed variant once ready — nothing renders a spinner or progress bar. For the optional turbo-frame UI (`image_tag`/`video_tag` with `async:`/`direct:`, a circular progress bar, and a retry affordance), add the companion gem [`active_storage-async_variants-ui`](https://github.com/botandrose/active_storage-async_variants-ui).
|
|
6
|
+
|
|
5
7
|
## The Problem
|
|
6
8
|
|
|
7
9
|
Active Storage's variant system assumes transformations are fast and reliable -- like generating an image thumbnail. But some transformations are slow (transcoding a 1GB video to 720p VP9) and fallible (the transcode may permanently fail). When you use `process: :later`, Active Storage enqueues a background job, but if the variant is requested before the job finishes, it falls through to synchronous processing -- blocking the request for minutes or timing out entirely. And if the transformation fails, the error bubbles up with no tracking or retry limits.
|
|
@@ -62,67 +64,22 @@ In views, use the same Active Storage helpers:
|
|
|
62
64
|
|
|
63
65
|
While the variant is still processing (or has failed), this serves the original video. Once processing completes, it serves the transcoded variant.
|
|
64
66
|
|
|
65
|
-
## `image_tag` / `video_tag` with `async:` and `direct:`
|
|
66
|
-
|
|
67
|
-
For a polish layer that shows a progress bar while a variant is being processed, polls for completion in the background, and optionally serves the finished variant straight from your CDN, the gem adds two options to `image_tag` and `video_tag`:
|
|
68
|
-
|
|
69
|
-
```erb
|
|
70
|
-
<%# Variant URL with the async client wired up: a progress bar while pending,
|
|
71
|
-
polls for completion, swaps to the real image when ready. %>
|
|
72
|
-
<%= image_tag user.avatar.variant(:web), async: true %>
|
|
73
|
-
|
|
74
|
-
<%# When the variant is ready, render its direct CDN/S3 URL instead of
|
|
75
|
-
routing through Rails. While pending/failed, falls back to the
|
|
76
|
-
Rails representation URL (which serves the original). %>
|
|
77
|
-
<%= image_tag user.avatar.variant(:web), direct: true %>
|
|
78
|
-
|
|
79
|
-
<%# Both together: direct URL once processed, progress bar while pending,
|
|
80
|
-
polling for completion. %>
|
|
81
|
-
<%= image_tag user.avatar.variant(:web), async: true, direct: true %>
|
|
82
|
-
|
|
83
|
-
<%# Same for videos. %>
|
|
84
|
-
<%= video_tag user.video.variant(:web), async: true, controls: true %>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
The first argument must be a `VariantWithRecord` or `Preview` when either option is set; otherwise `image_tag` / `video_tag` behave exactly as in stock Rails.
|
|
88
|
-
|
|
89
|
-
### Configure the direct URL host
|
|
90
|
-
|
|
91
|
-
By default `direct:` uses the storage service's URL (presigned for private buckets, unsigned for public). To serve from a CDN, set the host in an initializer:
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
# config/initializers/active_storage_async_variants.rb
|
|
95
|
-
ActiveStorage::AsyncVariants.cdn_host = "https://d1234abcd.cloudfront.net"
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The resulting URL is `"#{cdn_host}/#{variant.key}"`.
|
|
99
|
-
|
|
100
|
-
### JavaScript
|
|
101
|
-
|
|
102
|
-
No manual wiring is required. The async state partials are self-contained `<turbo-frame>`s. The only requirement is that the host app loads **Turbo** -- the gem depends on `turbo-rails`, which a default Rails app already includes.
|
|
103
|
-
|
|
104
67
|
## Configuration
|
|
105
68
|
|
|
106
|
-
Set options in an initializer. The `configure` block groups them (each is also a plain accessor, e.g. `ActiveStorage::AsyncVariants.
|
|
69
|
+
Set options in an initializer. The `configure` block groups them (each is also a plain accessor, e.g. `ActiveStorage::AsyncVariants.heartbeat_interval = …`):
|
|
107
70
|
|
|
108
71
|
```ruby
|
|
109
72
|
# config/initializers/active_storage_async_variants.rb
|
|
110
73
|
ActiveStorage::AsyncVariants.configure do |config|
|
|
111
|
-
config.cdn_host = "https://d1234abcd.cloudfront.net"
|
|
112
74
|
config.heartbeat_interval = 5.seconds
|
|
113
75
|
config.heartbeat_stale_after = 60.seconds
|
|
114
|
-
config.parent_controller = "ApplicationController"
|
|
115
|
-
config.retry_visible_if { current_user&.admin? }
|
|
116
76
|
end
|
|
117
77
|
```
|
|
118
78
|
|
|
119
79
|
| Option | Default | Purpose |
|
|
120
80
|
|--------|---------|---------|
|
|
121
|
-
| `
|
|
122
|
-
| `heartbeat_interval` | `5.seconds` | Expected cadence of progress heartbeats; the processing `<turbo-frame>` re-polls at this rate. |
|
|
81
|
+
| `heartbeat_interval` | `5.seconds` | Expected cadence of progress heartbeats from external transformers. (The UI gem's processing `<turbo-frame>` re-polls at this rate.) |
|
|
123
82
|
| `heartbeat_stale_after` | `60.seconds` | A processing variant with no heartbeat for this long is marked `failed`. Must exceed `heartbeat_interval`. |
|
|
124
|
-
| `parent_controller` | `"ActionController::Base"` | Base class for the gem's controllers, so the retry view can reach your app's `current_user`. Set as a String. |
|
|
125
|
-
| `retry_visible_if` | off | Block (run in the view context) gating the failed-state retry affordance. |
|
|
126
83
|
|
|
127
84
|
## Writing a Transformer
|
|
128
85
|
|
|
@@ -229,7 +186,7 @@ variant.error # => error message string, or nil
|
|
|
229
186
|
|
|
230
187
|
## Placeholders
|
|
231
188
|
|
|
232
|
-
There is nothing to configure. A variant's `.url` serves the original while it is pending, processing, or failed, and the processed variant once ready.
|
|
189
|
+
There is nothing to configure. A variant's `.url` serves the original while it is pending, processing, or failed, and the processed variant once ready. (For a circular progress bar while processing and a red error glyph once failed, add the [`active_storage-async_variants-ui`](https://github.com/botandrose/active_storage-async_variants-ui) gem.)
|
|
233
190
|
|
|
234
191
|
## Failure Handling
|
|
235
192
|
|
data/Rakefile
CHANGED
|
@@ -5,23 +5,4 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
task :cucumber do
|
|
10
|
-
sh "bundle exec cucumber"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
desc "Run rspec and cucumber"
|
|
14
|
-
task all: [:spec, :cucumber]
|
|
15
|
-
|
|
16
|
-
namespace :vendor do
|
|
17
|
-
desc "Pull the latest @botandrose/progress-bar from unpkg.com into app/assets"
|
|
18
|
-
task :progress_bar do
|
|
19
|
-
require "open-uri"
|
|
20
|
-
url = "https://unpkg.com/@botandrose/progress-bar"
|
|
21
|
-
dest = File.expand_path("app/assets/javascripts/progress-bar.js", __dir__)
|
|
22
|
-
File.write(dest, URI.open(url).read)
|
|
23
|
-
puts "Vendored #{url} -> #{dest}"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
task default: :all
|
|
8
|
+
task default: :spec
|
data/config/routes.rb
CHANGED
|
@@ -4,21 +4,4 @@ Rails.application.routes.draw do
|
|
|
4
4
|
post "/active_storage/async_variants/callbacks/:token",
|
|
5
5
|
to: "active_storage/async_variants/callbacks#create",
|
|
6
6
|
as: :active_storage_async_variant_callback
|
|
7
|
-
|
|
8
|
-
get "/active_storage/async_variants/states/:signed_blob_id/:variation_key",
|
|
9
|
-
to: "active_storage/async_variants/states#show",
|
|
10
|
-
as: :async_variant_state
|
|
11
|
-
post "/active_storage/async_variants/states/:signed_blob_id/:variation_key/retry",
|
|
12
|
-
to: "active_storage/async_variants/states#retry",
|
|
13
|
-
as: :async_variant_state_retry
|
|
14
|
-
|
|
15
|
-
# Tiny 1x1 GIF whose URL carries the media's filename, so the processing/failed
|
|
16
|
-
# box reserves layout without fetching the original through a representation.
|
|
17
|
-
get "/active_storage/async_variants/placeholder/:filename",
|
|
18
|
-
to: "active_storage/async_variants/states#placeholder",
|
|
19
|
-
as: :async_variant_placeholder,
|
|
20
|
-
constraints: { filename: %r{[^/]+} },
|
|
21
|
-
format: false
|
|
22
|
-
|
|
23
|
-
ActiveStorage::AsyncVariants::Assets.draw(self, "/active_storage/async_variants/assets")
|
|
24
7
|
end
|
data/gemfiles/rails_7.2.gemfile
CHANGED
data/gemfiles/rails_8.0.gemfile
CHANGED
data/gemfiles/rails_8.1.gemfile
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "turbo-rails"
|
|
4
|
-
require "isolate_assets"
|
|
5
3
|
require_relative "async_variants/version"
|
|
6
|
-
require_relative "async_variants/helper"
|
|
7
4
|
require_relative "async_variants/transformer"
|
|
8
5
|
require_relative "async_variants/registry"
|
|
9
6
|
require_relative "async_variants/blob_extension"
|
|
@@ -15,41 +12,18 @@ require_relative "async_variants/attachment_extension"
|
|
|
15
12
|
require_relative "async_variants/reflection_extension"
|
|
16
13
|
require_relative "async_variants/process_job"
|
|
17
14
|
require_relative "async_variants/heartbeat_watchdog_job"
|
|
18
|
-
require_relative "async_variants/asset_tag_helper_extension"
|
|
19
15
|
|
|
20
16
|
module ActiveStorage
|
|
21
17
|
module AsyncVariants
|
|
22
|
-
# HTML attributes round-tripped through the state-endpoint URL so the
|
|
23
|
-
# eventual processed-state render can apply them to the inner <img>/<video>.
|
|
24
|
-
PASS_THROUGH_HTML_OPTIONS = %i[alt width height controls autoplay preload].freeze
|
|
25
|
-
|
|
26
|
-
mattr_accessor :cdn_host
|
|
27
|
-
mattr_accessor :retry_visible_proc, default: ->(_view) { false }
|
|
28
|
-
|
|
29
|
-
# Lets the host app plug its auth chain (and thus `current_user`) into the
|
|
30
|
-
# gem's StatesController. Set to a string so resolution is deferred until
|
|
31
|
-
# the host's class is autoloadable. Defaults to ActionController::Base.
|
|
32
|
-
mattr_accessor :parent_controller, default: "ActionController::Base"
|
|
33
|
-
|
|
34
18
|
# How long an external transform may go without a heartbeat before the
|
|
35
19
|
# watchdog fails it. Must exceed the transformer's heartbeat interval.
|
|
36
20
|
mattr_accessor :heartbeat_stale_after, default: 60.seconds
|
|
37
21
|
|
|
38
|
-
# The transformer's heartbeat cadence.
|
|
39
|
-
#
|
|
22
|
+
# The transformer's heartbeat cadence. External services report at this
|
|
23
|
+
# rate; the UI layer's turbo-frame poll matches it. Keep heartbeat_stale_after
|
|
24
|
+
# well above it.
|
|
40
25
|
mattr_accessor :heartbeat_interval, default: 5.seconds
|
|
41
26
|
|
|
42
|
-
# Gates the failed-state retry affordance; the block runs in the view context.
|
|
43
|
-
def self.retry_visible_if(&block)
|
|
44
|
-
self.retry_visible_proc = block
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.retry_visible?(view)
|
|
48
|
-
!!view.instance_exec(&retry_visible_proc)
|
|
49
|
-
rescue StandardError
|
|
50
|
-
false
|
|
51
|
-
end
|
|
52
|
-
|
|
53
27
|
def self.configure
|
|
54
28
|
yield self
|
|
55
29
|
end
|
|
@@ -73,10 +47,6 @@ module ActiveStorage
|
|
|
73
47
|
# demand, and we just need the extensions in place by the time
|
|
74
48
|
# the first one loads.
|
|
75
49
|
ActiveStorage::AsyncVariants.prepend_model_extensions!
|
|
76
|
-
|
|
77
|
-
ActionView::Helpers::AssetTagHelper.prepend(
|
|
78
|
-
ActiveStorage::AsyncVariants::AssetTagHelperExtension
|
|
79
|
-
)
|
|
80
50
|
end
|
|
81
51
|
end
|
|
82
52
|
|
|
@@ -105,8 +75,6 @@ module ActiveStorage
|
|
|
105
75
|
)
|
|
106
76
|
end
|
|
107
77
|
|
|
108
|
-
Assets = IsolateAssets.register(namespace: self, engine: Engine, route_name: :async_variant_asset)
|
|
109
|
-
|
|
110
78
|
def self.callback_token_for(variant_record)
|
|
111
79
|
ActiveStorage.verifier.generate(variant_record.id, purpose: :async_variant_callback)
|
|
112
80
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_storage-async_variants
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activestorage
|
|
@@ -23,34 +23,6 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.2'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: turbo-rails
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '2.0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '2.0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: isolate_assets
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.4'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.4'
|
|
54
26
|
email:
|
|
55
27
|
- micah@botandrose.com
|
|
56
28
|
executables: []
|
|
@@ -66,34 +38,17 @@ files:
|
|
|
66
38
|
- LICENSE.txt
|
|
67
39
|
- README.md
|
|
68
40
|
- Rakefile
|
|
69
|
-
- app/assets/javascripts/progress-bar.js
|
|
70
|
-
- app/assets/javascripts/retry.js
|
|
71
|
-
- app/assets/stylesheets/progress.css
|
|
72
|
-
- app/assets/stylesheets/retry.css
|
|
73
41
|
- app/controllers/active_storage/async_variants/callbacks_controller.rb
|
|
74
|
-
- app/controllers/active_storage/async_variants/states_controller.rb
|
|
75
|
-
- app/views/active_storage/async_variants/states/_failed.html.erb
|
|
76
|
-
- app/views/active_storage/async_variants/states/_pending.html.erb
|
|
77
|
-
- app/views/active_storage/async_variants/states/_processed.html.erb
|
|
78
|
-
- app/views/active_storage/async_variants/states/_processing.html.erb
|
|
79
|
-
- app/views/active_storage/async_variants/states/show.html.erb
|
|
80
42
|
- config/routes.rb
|
|
81
43
|
- db/migrate/20260607000001_add_heartbeat_columns_to_variant_records.rb
|
|
82
44
|
- db/migrate/20260608000001_add_created_at_to_variant_records.rb
|
|
83
|
-
- features/retry_flow.feature
|
|
84
|
-
- features/retry_visibility.feature
|
|
85
|
-
- features/state_rendering.feature
|
|
86
|
-
- features/step_definitions/dummy_steps.rb
|
|
87
|
-
- features/support/env.rb
|
|
88
45
|
- gemfiles/rails_7.2.gemfile
|
|
89
46
|
- gemfiles/rails_8.0.gemfile
|
|
90
47
|
- gemfiles/rails_8.1.gemfile
|
|
91
48
|
- lib/active_storage/async_variants.rb
|
|
92
|
-
- lib/active_storage/async_variants/asset_tag_helper_extension.rb
|
|
93
49
|
- lib/active_storage/async_variants/attachment_extension.rb
|
|
94
50
|
- lib/active_storage/async_variants/blob_extension.rb
|
|
95
51
|
- lib/active_storage/async_variants/heartbeat_watchdog_job.rb
|
|
96
|
-
- lib/active_storage/async_variants/helper.rb
|
|
97
52
|
- lib/active_storage/async_variants/preview_extension.rb
|
|
98
53
|
- lib/active_storage/async_variants/process_job.rb
|
|
99
54
|
- lib/active_storage/async_variants/reflection_extension.rb
|