active_storage-async_variants 0.6.0 → 0.7.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: f3d828692f86ff7b409b93f3198daae20c5c6821a0cce10da31b625abe7c04be
4
- data.tar.gz: '0319ea7873525ea5d76a930e195687b3ba28bebd58922e3fc4aa821238241f28'
3
+ metadata.gz: 259c11526314988d60e78e9e176dc16105c6aeb578b1774693a666fc1e277a21
4
+ data.tar.gz: 92fc3546ed708ed709ad4fedc2d7e6258d765719fad75571844671a902f1b485
5
5
  SHA512:
6
- metadata.gz: 9e80ebc6059bf84853cf7bf05d824ed5d25d5b195a4702d86495cdb29366eb02ee390558768fa66a1f5ea85e8e86987a46f9c7a2492de59e1a7287b25ef7074a
7
- data.tar.gz: 23dc024c9a1ceaa334580d32af52f20e4e327f3d40d22e438616bdc0077f5b62f95deb50670ee5a30b3a1e72716e2ef9604bd6eabe2fc3a699aaa21142518ad8
6
+ metadata.gz: ebfe0839de2603c91a5705f3942b30b3db7685d20ce5d3d2425f98472a191d559ac9103bab5a59972e09f18385b8829283fe0323f2de4810e8619cb57e028ab2
7
+ data.tar.gz: 165f5796f098a3f40194a9d893ba07b98bd7b3a7ac8601fda744f5dcb1e4a98c0d69e1514e788f612ea8fa1dbf4bca6c44cbb7ee5ef072fcf5fa13fe03ce5b56
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.7.0]
2
+
3
+ - **Breaking:** A variant now opts into async processing with `async: true` instead of `processing:`. The `processing:` and `failed:` placeholder options are removed entirely — there is nothing to configure. A variant's `.url` serves the original while pending/processing/failed, and the processed variant once ready.
4
+ - The `async:`-helper UI no longer fetches a placeholder through the representations path on every poll. The processing/failed states render a zero-network sized box with the progress bar floating over it.
5
+ - The failed state now renders the progress bar in its error state (a static red ring with an X, via `@botandrose/progress-bar`'s `error` attribute) rather than a configurable failed SVG. The retry affordance is unchanged.
6
+ - Migration note: replace `processing: <anything>` with `async: true` on each async variant and drop any `failed:` option. Any `:original`/`:blank`/String/Proc placeholder values no longer have an effect.
7
+
1
8
  ## [0.6.0]
2
9
 
3
10
  - External transforms can now report progress: a `progress` callback records the percent complete and a heartbeat, readable via `#progress` / `#progress_known?` on variants and previews.
data/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## What This Is
6
6
 
7
- A Rails engine gem that extends Active Storage with async-safe variant processing. Solves the problem where slow transformations (e.g., video transcoding) block requests or fail silently. The `processing:` option on a variant definition opts it into async processing.
7
+ A Rails engine gem that extends Active Storage with async-safe variant processing. Solves the problem where slow transformations (e.g., video transcoding) block requests or fail silently. The `async: true` option on a variant definition opts it into async processing.
8
8
 
9
9
  ## Commands
10
10
 
@@ -19,9 +19,9 @@ bundle exec rspec spec/active_storage/async_variants_spec.rb -e "description" #
19
19
 
20
20
  The gem works by prepending extension modules onto Active Storage classes:
21
21
 
22
- - **`VariationExtension`** → `ActiveStorage::Variation` — extracts async options (`processing:`, `failed:`, `transformer:`) from variant config before passing the rest to standard Active Storage
23
- - **`AttachmentExtension`** → `ActiveStorage::Attachment` — hooks into `transform_variants_later` to enqueue `ProcessJob` for variants with `processing:`
24
- - **`VariantWithRecordExtension`** → `ActiveStorage::VariantWithRecord` — overrides URL generation to serve the `processing:` (or `failed:`) placeholder while not ready; adds state query methods (`processed?`, `processing?`, `pending?`, `failed?`)
22
+ - **`VariationExtension`** → `ActiveStorage::Variation` — extracts async options (`async:`, `transformer:`) from variant config before passing the rest to standard Active Storage
23
+ - **`AttachmentExtension`** → `ActiveStorage::Attachment` — hooks into `transform_variants_later` to enqueue `ProcessJob` for variants with `async: true`
24
+ - **`VariantWithRecordExtension`** → `ActiveStorage::VariantWithRecord` — overrides URL generation to serve the original blob while not ready; adds state query methods (`processed?`, `processing?`, `pending?`, `failed?`)
25
25
  - **`ProcessJob`** — background job that determines transformer type (inline vs external) and processes accordingly
26
26
 
27
27
  ### Transformer Types
data/README.md CHANGED
@@ -19,7 +19,7 @@ bin/rails db:migrate
19
19
 
20
20
  ## Usage
21
21
 
22
- Add `processing:` to any named variant to opt into the async pipeline. The value is what to serve while the variant is being processed:
22
+ Add `async: true` to any named variant to opt into the async pipeline:
23
23
 
24
24
  ```ruby
25
25
  class User < ApplicationRecord
@@ -28,12 +28,12 @@ class User < ApplicationRecord
28
28
  transformer: VideoTranscoder,
29
29
  codec: "vp9",
30
30
  resolution: "720p",
31
- processing: :original
31
+ async: true
32
32
  end
33
33
  end
34
34
  ```
35
35
 
36
- The presence of `processing:` is what opts a variant into async processing. Without it, variants behave exactly as they do in standard Active Storage. The `transformer:` option is independent -- you can use a custom transformer synchronously, or use the default transformer asynchronously:
36
+ The presence of `async: true` is what opts a variant into async processing. Without it, variants behave exactly as they do in standard Active Storage. The `transformer:` option is independent -- you can use a custom transformer synchronously, or use the default transformer asynchronously:
37
37
 
38
38
  ```ruby
39
39
  has_one_attached :video do |attachable|
@@ -41,12 +41,12 @@ has_one_attached :video do |attachable|
41
41
  attachable.variant :web,
42
42
  transformer: VideoTranscoder,
43
43
  codec: "vp9",
44
- processing: :original
44
+ async: true
45
45
 
46
46
  # Async with default transformer (large image resize that's too slow for inline)
47
47
  attachable.variant :thumbnail,
48
48
  resize_to_limit: [200, 200],
49
- processing: :original
49
+ async: true
50
50
 
51
51
  # Sync with custom transformer (fast custom processing, no opt-in needed)
52
52
  attachable.variant :watermarked,
@@ -60,23 +60,23 @@ In views, use the same Active Storage helpers:
60
60
  <%= video_tag user.video.variant(:web).url %>
61
61
  ```
62
62
 
63
- If the variant is still processing, this serves the original video. Once processing completes, it serves the transcoded variant.
63
+ While the variant is still processing (or has failed), this serves the original video. Once processing completes, it serves the transcoded variant.
64
64
 
65
65
  ## `image_tag` / `video_tag` with `async:` and `direct:`
66
66
 
67
- For a polish layer that swaps in placeholder content 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`:
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
68
 
69
69
  ```erb
70
- <%# Variant URL with the async client wired up: spinner while pending,
70
+ <%# Variant URL with the async client wired up: a progress bar while pending,
71
71
  polls for completion, swaps to the real image when ready. %>
72
72
  <%= image_tag user.avatar.variant(:web), async: true %>
73
73
 
74
74
  <%# When the variant is ready, render its direct CDN/S3 URL instead of
75
75
  routing through Rails. While pending/failed, falls back to the
76
- Rails representation URL (which serves the placeholder). %>
76
+ Rails representation URL (which serves the original). %>
77
77
  <%= image_tag user.avatar.variant(:web), direct: true %>
78
78
 
79
- <%# Both together: direct URL once processed, placeholder while pending,
79
+ <%# Both together: direct URL once processed, progress bar while pending,
80
80
  polling for completion. %>
81
81
  <%= image_tag user.avatar.variant(:web), async: true, direct: true %>
82
82
 
@@ -227,31 +227,9 @@ variant.failed? # => true if permanently failed
227
227
  variant.error # => error message string, or nil
228
228
  ```
229
229
 
230
- ## Placeholder Options
230
+ ## Placeholders
231
231
 
232
- The `processing:` option controls what gets served while a variant is being processed. The `failed:` option (optional) controls what gets served once the variant has permanently failed; if omitted, it defaults to the `processing:` value.
233
-
234
- ```ruby
235
- # Serve the original unprocessed file while processing
236
- attachable.variant :web, processing: :original
237
-
238
- # Return nil -- let the view handle it
239
- attachable.variant :web, processing: :blank
240
-
241
- # Static URL while processing
242
- attachable.variant :web, processing: "/placeholders/processing.svg"
243
-
244
- # Dynamic placeholder via Proc
245
- attachable.variant :web,
246
- processing: -> (blob) { "/placeholders/processing.svg" }
247
-
248
- # Distinct placeholder when permanently failed
249
- attachable.variant :web,
250
- processing: :original,
251
- failed: "/icons/broken.svg"
252
- ```
253
-
254
- Both options accept the same set of values: `:original`, `:blank`, a String URL, or a Proc that receives the blob.
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.
255
233
 
256
234
  ## Failure Handling
257
235
 
@@ -276,7 +254,7 @@ variant.error # => "ffmpeg exited with status 1: ..."
276
254
  5. The external service processes the file, uploads the result to the destination URL
277
255
  6. The external service POSTs to the callback URL with success/failure status
278
256
  7. The gem's callback endpoint transitions the `VariantRecord` to `processed` or `failed`
279
- 8. When a view requests the variant URL, the gem checks state and serves the variant or the `processing:`/`failed:` placeholder
257
+ 8. When a view requests the variant URL, the gem checks state and serves the processed variant or, while pending/processing/failed, the original
280
258
 
281
259
  ### Inline transformer flow
282
260
 
@@ -284,7 +262,7 @@ variant.error # => "ffmpeg exited with status 1: ..."
284
262
  4. The job calls the transformer's `process` method, blocking until complete
285
263
  5. On success, the output is uploaded, the `VariantRecord` transitions to `processed`
286
264
  6. On failure, the error is recorded and the job is re-enqueued (up to 3 attempts)
287
- 7. When a view requests the variant URL, the gem checks state and serves the variant or the `processing:`/`failed:` placeholder
265
+ 7. When a view requests the variant URL, the gem checks state and serves the processed variant or, while pending/processing/failed, the original
288
266
 
289
267
  ## License
290
268
 
@@ -98,8 +98,13 @@ const STYLES = `
98
98
  stroke: var(--indeterminate-color);
99
99
  }
100
100
 
101
- :host([error]) .progress-ring {
101
+ /* The X of the error glyph; revealed by [error]. */
102
+ .error-mark {
103
+ display: none;
104
+ fill: none;
102
105
  stroke: var(--error-color);
106
+ stroke-width: var(--circular-thickness);
107
+ stroke-linecap: round;
103
108
  }
104
109
 
105
110
  .label {
@@ -132,6 +137,12 @@ const STYLES = `
132
137
  0% { transform: rotate(-90deg); }
133
138
  100% { transform: rotate(270deg); }
134
139
  }
140
+
141
+ /* Error (circular): static full ring + X. Last so it beats the indeterminate rules. */
142
+ :host([error]) .track { stroke: var(--error-color); }
143
+ :host([error]) .progress-ring { display: none; }
144
+ :host([error]) .ring { animation: none; }
145
+ :host([error]) .error-mark { display: block; }
135
146
  `;
136
147
 
137
148
  const LINEAR_HTML = `
@@ -144,6 +155,7 @@ const CIRCULAR_HTML = `
144
155
  <svg class="ring" viewBox="0 0 100 100">
145
156
  <circle class="track" cx="50" cy="50" r="${CIRCLE_RADIUS}"></circle>
146
157
  <circle class="progress-ring" cx="50" cy="50" r="${CIRCLE_RADIUS}" pathLength="${PATH_LENGTH}"></circle>
158
+ <path class="error-mark" d="M37 37 L63 63 M63 37 L37 63"></path>
147
159
  </svg>
148
160
  <span class="label"><slot></slot></span>
149
161
  </div>
@@ -2,19 +2,23 @@
2
2
  media box (and the progress bar centered on it) coincides with the parent
3
3
  figure box, instead of sitting in a taller line box from the baseline gap --
4
4
  otherwise an overlay centered on the figure lands off-center from the ring. */
5
- turbo-frame:has(> .async-variant-processing) {
5
+ turbo-frame:has(> .async-variant-processing),
6
+ turbo-frame:has(> .async-variant-failed) {
6
7
  display: block;
7
8
  }
8
- .async-variant-processing {
9
+ .async-variant-processing,
10
+ .async-variant-failed {
9
11
  position: relative;
10
12
  display: block;
11
13
  }
12
- /* Present to reserve the processed media's box, but rendered invisible via
13
- opacity rather than visibility/display -- so it still has layout and stays
14
- queryable by Capybara. The spinner floats over it. block drops the baseline
15
- descender so the ring centers on the media, not a few px below it. */
14
+ /* Present to reserve the variant's box, but rendered invisible via opacity
15
+ rather than visibility/display -- so it still has layout and stays queryable
16
+ by Capybara. The progress bar floats over it. block drops the baseline
17
+ descender so the ring centers on the box, not a few px below it. */
16
18
  .async-variant-processing img,
17
- .async-variant-processing video {
19
+ .async-variant-processing video,
20
+ .async-variant-failed img,
21
+ .async-variant-failed video {
18
22
  opacity: 0.001;
19
23
  display: block;
20
24
  }
@@ -27,4 +31,5 @@ turbo-frame:has(> .async-variant-processing) {
27
31
  --progress-color: #2E7D32;
28
32
  --indeterminate-color: #2E7D32;
29
33
  --track-color: rgba(0, 0, 0, 0.25);
34
+ --error-color: #ff7c81;
30
35
  }
@@ -1,16 +1,17 @@
1
1
  <div class="async-variant-state async-variant-failed">
2
- <%= image_tag async_variant_representation_path(variant), **html_options.symbolize_keys.except(:controls, :autoplay, :preload) %>
2
+ <%= async_variant_placeholder_tag(variant, html_options, kind: kind) %>
3
+
4
+ <%= content_tag "progress-bar", "", mode: "circular", error: "", class: "async-variant-progress" %>
5
+
6
+ <%= ActiveStorage::AsyncVariants.stylesheet_link_tag "progress" %>
7
+ <%= ActiveStorage::AsyncVariants.javascript_include_tag "progress-bar", type: "module" %>
3
8
 
4
9
  <% if ActiveStorage::AsyncVariants.retry_visible?(self) %>
5
10
  <% dialog_id = "#{async_variant_frame_id(variant)}-error" %>
6
11
  <% form_id = "#{dialog_id}-form" %>
7
12
  <%# Light DOM, so the custom property inherits into the shadow root. %>
8
13
  <style>
9
- .async-variant-failed {
10
- position: relative;
11
- display: inline-block;
12
- --retry-opener-display: none;
13
- }
14
+ .async-variant-failed { --retry-opener-display: none; }
14
15
  .async-variant-failed:hover { --retry-opener-display: inline-flex; }
15
16
  </style>
16
17
  <%# In the light DOM so its submit reaches Turbo, and so preventDefault on the
@@ -1,6 +1,5 @@
1
1
  <div class="async-variant-state async-variant-processing">
2
- <% method = kind == :video ? :video_tag : :image_tag %>
3
- <%= send method, async_variant_representation_path(variant), **html_options.symbolize_keys %>
2
+ <%= async_variant_placeholder_tag(variant, html_options, kind: kind) %>
4
3
 
5
4
  <%= content_tag "progress-bar", "", mode: "circular", class: "async-variant-progress", percent: variant.progress %>
6
5
  </div>
@@ -37,3 +37,4 @@ Feature: image_tag with async: true emits the right markup per variant state
37
37
  When I visit the avatar page for the :thumb_proc variant
38
38
  Then the page should contain a turbo-frame
39
39
  And the frame should render the failed state
40
+ And the frame should render an error progress bar
@@ -24,6 +24,10 @@ Then /^the frame should render a progress bar$/ do
24
24
  expect(page).to have_css("turbo-frame progress-bar", visible: :all)
25
25
  end
26
26
 
27
+ Then /^the frame should render an error progress bar$/ do
28
+ expect(page).to have_css("turbo-frame progress-bar[error]", visible: :all)
29
+ end
30
+
27
31
  Then /^the progress bar should be indeterminate$/ do
28
32
  expect(page).to have_css("turbo-frame progress-bar:not([percent])", visible: :all)
29
33
  end
@@ -14,7 +14,7 @@ module ActiveStorage
14
14
  return unless blob.bucket_backed?
15
15
 
16
16
  named_variants.each do |name, named_variant|
17
- next unless named_variant.transformations.key?(:processing)
17
+ next unless named_variant.transformations.key?(:async)
18
18
 
19
19
  ActiveStorage::AsyncVariants::ProcessJob.perform_later(
20
20
  record, self.name, name.to_s,
@@ -7,6 +7,25 @@ module ActiveStorage
7
7
  # `helper ActiveStorage::AsyncVariants::Helper` (so the state partials can
8
8
  # use them).
9
9
  module Helper
10
+ # 1x1 transparent GIF: reserves the variant's box without fetching the
11
+ # original through the representations path on every poll.
12
+ PLACEHOLDER_SRC = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
13
+
14
+ def async_variant_placeholder_tag(variant, html_options = {}, kind: :image)
15
+ opts = async_variant_box_dimensions(variant)
16
+ .merge(html_options.symbolize_keys.except(:src, :controls, :autoplay, :preload, :poster))
17
+ # A video placeholder carries no <source>, so it reserves the box
18
+ # without loading anything -- same zero-network intent as the GIF.
19
+ kind == :video ? content_tag(:video, "", opts) : image_tag(PLACEHOLDER_SRC, opts)
20
+ end
21
+
22
+ def async_variant_box_dimensions(variant)
23
+ resize = variant.variation.transformations
24
+ .values_at(:resize_to_limit, :resize_to_fit, :resize_to_fill)
25
+ .compact.first
26
+ resize.is_a?(Array) ? { width: resize[0], height: resize[1] } : {}
27
+ end
28
+
10
29
  # In test with non-bucket-backed services, the gem defers to vanilla
11
30
  # ActiveStorage (synchronous vips transform) -- inline rendering keeps
12
31
  # those environments simple. Otherwise, only inline a normal <img> when
@@ -67,15 +67,15 @@ module ActiveStorage
67
67
  private
68
68
 
69
69
  def async_preview?
70
- resolved_async_options[:transformer].present?
70
+ resolved_async_options[:async].present?
71
71
  end
72
72
 
73
73
  # Variations rebuilt from the redirect URL only carry transformations --
74
- # :transformer / :processing / :failed are stripped at Variation#initialize
75
- # and not embedded in the URL key. Recover them via the digest-keyed
76
- # registry that VariationExtension warms on every view-side variant call,
77
- # or fall back to scanning attached named variants when the registry is
78
- # cold (autoloader hasn't touched the consumer model yet).
74
+ # :async / :transformer are stripped at Variation#initialize and not
75
+ # embedded in the URL key. Recover them via the digest-keyed registry that
76
+ # VariationExtension warms on every view-side variant call, or fall back to
77
+ # scanning attached named variants when the registry is cold (autoloader
78
+ # hasn't touched the consumer model yet).
79
79
  def resolved_async_options
80
80
  @resolved_async_options ||=
81
81
  variation.async_options.presence ||
@@ -90,7 +90,7 @@ module ActiveStorage
90
90
  attachment.send(:named_variants).each do |name, _|
91
91
  candidate = attachment.variant(name.to_sym)
92
92
  next unless candidate.variation.transformations.to_json == target
93
- return [attachment, name, candidate.variation.async_options] if candidate.variation.async_options[:transformer].present?
93
+ return [attachment, name, candidate.variation.async_options] if candidate.variation.async_options[:async]
94
94
  end
95
95
  end
96
96
  nil
@@ -107,25 +107,9 @@ module ActiveStorage
107
107
  blob.variant_records.find_by(variation_digest: variation.digest)
108
108
  end
109
109
 
110
+ # Serve the original until the preview variant is processed.
110
111
  def fallback_preview_url(...)
111
- case active_fallback
112
- when :original then blob.url(...)
113
- when :blank then nil
114
- when Proc then active_fallback.call(blob)
115
- when String then active_fallback
116
- end
117
- end
118
-
119
- def active_fallback
120
- if failed?
121
- resolved_async_options.fetch(:failed) { resolved_async_options[:processing] }
122
- else
123
- resolved_async_options[:processing]
124
- end
125
- end
126
-
127
- def failed?
128
- find_preview_variant_record&.state == "failed"
112
+ blob.url(...)
129
113
  end
130
114
  end
131
115
  end
@@ -7,7 +7,7 @@ module ActiveStorage
7
7
  #
8
8
  # has_one_attached :avatar do |attachable|
9
9
  # attachable.variant :thumb, resize_to_limit: [150, 150], format: "webp",
10
- # transformer: Crucible, processing: "/spinner.svg"
10
+ # transformer: Crucible, async: true
11
11
  # end
12
12
  #
13
13
  # Without this, the Registry only warms when view-side code calls
@@ -7,7 +7,7 @@ module ActiveStorage
7
7
  # Process-wide lookup of async_options keyed by Variation#digest.
8
8
  #
9
9
  # Populated lazily by VariationExtension#initialize whenever a Variation is
10
- # constructed with :transformer in its async_options -- which happens on
10
+ # constructed with :async in its async_options -- which happens on
11
11
  # every view-side `attachment.variant(:name)` call. The redirect controller
12
12
  # then resolves async_options by digest without scanning blob.attachments
13
13
  # for a transformations-match.
@@ -13,8 +13,8 @@ module ActiveStorage
13
13
 
14
14
  # processed and failed are terminal states: cache fragments built when
15
15
  # state was pending/processing need to be invalidated so the next
16
- # render sees the new state (and serves the failed: fallback URL,
17
- # swaps src to the direct CDN URL, etc.).
16
+ # render sees the new state (swaps the processing box for the failed
17
+ # error state, swaps src to the direct CDN URL, etc.).
18
18
  def reached_terminal_state?
19
19
  saved_change_to_state? && %w[processed failed].include?(state)
20
20
  end
@@ -27,15 +27,8 @@ module ActiveStorage
27
27
  end
28
28
 
29
29
  def url(...)
30
- if blob.bucket_backed? && !processed?
31
- fallback = active_fallback
32
- case fallback
33
- when :original then blob.url(...)
34
- when :blank then nil
35
- when Proc then fallback.call(blob)
36
- when String then fallback
37
- else super
38
- end
30
+ if blob.bucket_backed? && async_variant? && !processed?
31
+ blob.url(...)
39
32
  else
40
33
  super
41
34
  end
@@ -84,6 +77,10 @@ module ActiveStorage
84
77
 
85
78
  private
86
79
 
80
+ def async_variant?
81
+ resolved_async_options[:async].present?
82
+ end
83
+
87
84
  def resolved_async_options
88
85
  @resolved_async_options ||=
89
86
  variation.async_options.presence ||
@@ -92,14 +89,6 @@ module ActiveStorage
92
89
  {}
93
90
  end
94
91
 
95
- def active_fallback
96
- if failed?
97
- resolved_async_options.fetch(:failed) { resolved_async_options[:processing] }
98
- else
99
- resolved_async_options[:processing]
100
- end
101
- end
102
-
103
92
  # Cold-path scan: used by enqueue! (which needs the attachment +
104
93
  # variant_name to dispatch ProcessJob) and by resolved_async_options as
105
94
  # a fallback when the Registry is cold (e.g. in dev, when a
@@ -126,7 +115,7 @@ module ActiveStorage
126
115
  attachment.send(:named_variants).each do |name, _|
127
116
  candidate = attachment.variant(name.to_sym)
128
117
  if candidate.variation.transformations.to_json == target
129
- return [attachment, name, candidate.variation.async_options] if candidate.variation.async_options[:processing].present?
118
+ return [attachment, name, candidate.variation.async_options] if candidate.variation.async_options[:async]
130
119
  end
131
120
  end
132
121
  end
@@ -3,7 +3,7 @@
3
3
  module ActiveStorage
4
4
  module AsyncVariants
5
5
  module VariationExtension
6
- ASYNC_KEYS = %i[processing failed transformer].freeze
6
+ ASYNC_KEYS = %i[async transformer].freeze
7
7
 
8
8
  def initialize(transformations)
9
9
  if transformations.is_a?(Hash)
@@ -13,7 +13,7 @@ module ActiveStorage
13
13
  @async_options = {}
14
14
  super
15
15
  end
16
- ActiveStorage::AsyncVariants::Registry.register(digest, @async_options) if @async_options[:processing].present?
16
+ ActiveStorage::AsyncVariants::Registry.register(digest, @async_options) if @async_options[:async]
17
17
  end
18
18
 
19
19
  def async_options
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveStorage
4
4
  module AsyncVariants
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-06-07 00:00:00.000000000 Z
10
+ date: 2026-06-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activestorage