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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34c672d7d6d989bb7c633aa1e57e081d6879a8ea9695ffd65997b9df9cf8f8ab
4
- data.tar.gz: aca54bb9b2ae6af16016f1e96e6e3f221bc7029b8eca9c145535eadefeb57d66
3
+ metadata.gz: d60acebdeb8fe9482d73576e79abb4737fb052e6df939e09829b9c0e766b9e12
4
+ data.tar.gz: 68be3b6ef1f9cb34e5a08f87b20470520367e16fdcc81ec73f18d5956ab8979a
5
5
  SHA512:
6
- metadata.gz: e86b3777c71d70d2014fe439fb91f395f1711cce40efefa3c5e5291a22bd2fc6d1933aad3a1c095557726e45547d7112f9e2f2bbd24fece75726adebddd6f686
7
- data.tar.gz: 591deb4316c685316084ebb26904e5fe5b2ecbea0328cc832469ae4185388239511e35507b53264e137cf85153bfe1a846e7bd0491ba023bb991333653cfb532
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.cdn_host = …`):
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
- | `cdn_host` | `nil` | Host for `direct:` URLs (`"#{cdn_host}/#{variant.key}"`); falls back to the storage service URL. |
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. With `async: true` on `image_tag`/`video_tag`, the gem instead shows a circular progress bar while processing, and a red error glyph -- the same progress bar in its error state -- once the variant has permanently failed.
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
- desc "Run browser-level acceptance tests (cucumber + cuprite against spec/dummy)"
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
@@ -10,9 +10,5 @@ gem "simplecov", require: false
10
10
  gem "rspec-rails"
11
11
  gem "rails", "~> 7.2.0"
12
12
  gem "sqlite3"
13
- gem "cucumber"
14
- gem "capybara"
15
- gem "cuprite"
16
- gem "puma"
17
13
 
18
14
  gemspec path: "../"
@@ -10,9 +10,5 @@ gem "simplecov", require: false
10
10
  gem "rspec-rails"
11
11
  gem "rails", "~> 8.0.0"
12
12
  gem "sqlite3"
13
- gem "cucumber"
14
- gem "capybara"
15
- gem "cuprite"
16
- gem "puma"
17
13
 
18
14
  gemspec path: "../"
@@ -10,9 +10,5 @@ gem "simplecov", require: false
10
10
  gem "rspec-rails"
11
11
  gem "rails", "~> 8.1.0"
12
12
  gem "sqlite3"
13
- gem "cucumber"
14
- gem "capybara"
15
- gem "cuprite"
16
- gem "puma"
17
13
 
18
14
  gemspec path: "../"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveStorage
4
4
  module AsyncVariants
5
- VERSION = "0.8.0"
5
+ VERSION = "0.9.0"
6
6
  end
7
7
  end
@@ -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. The turbo-frame poll matches it so
39
- # each reload lands on fresh progress; keep heartbeat_stale_after well above it.
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.8.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-08 00:00:00.000000000 Z
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