active_storage-async_variants 0.1.0 → 0.3.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/.github/workflows/ci.yml +0 -3
- data/CHANGELOG.md +10 -1
- data/CLAUDE.md +4 -4
- data/README.md +87 -27
- data/app/assets/javascripts/active_storage_async_variants.js +169 -0
- data/app/controllers/active_storage/async_variants/callbacks_controller.rb +34 -1
- data/gemfiles/rails_7.2.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/lib/active_storage/async_variants/asset_tag_helper_extension.rb +77 -0
- data/lib/active_storage/async_variants/attachment_extension.rb +3 -1
- data/lib/active_storage/async_variants/blob_extension.rb +15 -0
- data/lib/active_storage/async_variants/preview_extension.rb +117 -0
- data/lib/active_storage/async_variants/process_job.rb +6 -7
- data/lib/active_storage/async_variants/reflection_extension.rb +35 -0
- data/lib/active_storage/async_variants/registry.rb +38 -0
- data/lib/active_storage/async_variants/representations_redirect_controller_extension.rb +26 -0
- data/lib/active_storage/async_variants/variant_record_extension.rb +29 -0
- data/lib/active_storage/async_variants/variant_with_record_extension.rb +78 -19
- data/lib/active_storage/async_variants/variation_extension.rb +2 -1
- data/lib/active_storage/async_variants/version.rb +1 -1
- data/lib/active_storage/async_variants.rb +69 -8
- metadata +10 -5
- data/gemfiles/rails_7.2.gemfile.lock +0 -269
- data/gemfiles/rails_8.0.gemfile.lock +0 -266
- data/gemfiles/rails_8.1.gemfile.lock +0 -269
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
module AsyncVariants
|
|
5
|
+
module PreviewExtension
|
|
6
|
+
# Enqueue (or no-op if already done) the same ProcessJob the
|
|
7
|
+
# VariantWithRecord path uses, so Preview-side and VariantWithRecord-side
|
|
8
|
+
# processing converge on a single record-and-job machinery rather than
|
|
9
|
+
# the gem's earlier two-path design (one writing to preview_image's
|
|
10
|
+
# variant_records, one to the original blob's).
|
|
11
|
+
def processed
|
|
12
|
+
if async_preview?
|
|
13
|
+
enqueue_async_preview unless preview_variant_processed?
|
|
14
|
+
self
|
|
15
|
+
else
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def processed?
|
|
21
|
+
async_preview? ? preview_variant_processed? : super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def url(...)
|
|
25
|
+
if async_preview?
|
|
26
|
+
preview_variant_processed? ? find_preview_variant_record.image.url(...) : fallback_preview_url(...)
|
|
27
|
+
else
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key
|
|
33
|
+
if async_preview?
|
|
34
|
+
raise ActiveStorage::Preview::UnprocessedError unless preview_variant_processed?
|
|
35
|
+
find_preview_variant_record.image.blob.key
|
|
36
|
+
else
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def async_state
|
|
42
|
+
return nil unless async_preview?
|
|
43
|
+
find_preview_variant_record&.state || "pending"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def async_preview?
|
|
49
|
+
resolved_async_options[:transformer].present?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Variations rebuilt from the redirect URL only carry transformations --
|
|
53
|
+
# :transformer / :processing / :failed are stripped at Variation#initialize
|
|
54
|
+
# and not embedded in the URL key. Recover them via the digest-keyed
|
|
55
|
+
# registry that VariationExtension warms on every view-side variant call.
|
|
56
|
+
def resolved_async_options
|
|
57
|
+
@resolved_async_options ||=
|
|
58
|
+
variation.async_options.presence ||
|
|
59
|
+
ActiveStorage::AsyncVariants::Registry[variation.digest] ||
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def preview_variant_processed?
|
|
64
|
+
find_preview_variant_record&.state == "processed"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ProcessJob stores variant_records on the source blob (i.e. @variant.blob,
|
|
68
|
+
# which for a named variant declared on a previewable attachment is the
|
|
69
|
+
# original blob -- not preview_image.blob). Read from the same place.
|
|
70
|
+
def find_preview_variant_record
|
|
71
|
+
blob.variant_records.find_by(variation_digest: variation.digest)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Delegate to the named-variant VariantWithRecord so we go through the
|
|
75
|
+
# exact same enqueue_processing + ProcessJob machinery as direct
|
|
76
|
+
# variant calls. Skips silently if no matching named variant exists,
|
|
77
|
+
# which can happen for raw transformations that don't correspond to
|
|
78
|
+
# any declared async variant. Also skipped on non-bucket services,
|
|
79
|
+
# where the gem defers to vanilla ActiveStorage and dispatching here
|
|
80
|
+
# would synchronously transform via vips (broken for video blobs).
|
|
81
|
+
def enqueue_async_preview
|
|
82
|
+
return unless blob.bucket_backed?
|
|
83
|
+
target = variation.transformations.to_json
|
|
84
|
+
blob.attachments.each do |attachment|
|
|
85
|
+
attachment.send(:named_variants).each do |name, _|
|
|
86
|
+
candidate = attachment.variant(name.to_sym)
|
|
87
|
+
next unless candidate.variation.transformations.to_json == target
|
|
88
|
+
next unless candidate.variation.async_options[:processing].present?
|
|
89
|
+
candidate.processed
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def fallback_preview_url(...)
|
|
96
|
+
case active_fallback
|
|
97
|
+
when :original then blob.url(...)
|
|
98
|
+
when :blank then nil
|
|
99
|
+
when Proc then active_fallback.call(blob)
|
|
100
|
+
when String then active_fallback
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def active_fallback
|
|
105
|
+
if failed?
|
|
106
|
+
resolved_async_options.fetch(:failed) { resolved_async_options[:processing] }
|
|
107
|
+
else
|
|
108
|
+
resolved_async_options[:processing]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def failed?
|
|
113
|
+
find_preview_variant_record&.state == "failed"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module ActiveStorage
|
|
4
4
|
module AsyncVariants
|
|
5
5
|
class ProcessJob < ActiveJob::Base
|
|
6
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3 do |job, error|
|
|
7
|
+
job.logger.error "AsyncVariants: permanently failed: #{error.message}"
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
def perform(record, attachment_name, variant_name)
|
|
7
11
|
attachment = record.public_send(attachment_name)
|
|
8
12
|
@variant = attachment.variant(variant_name.to_sym)
|
|
@@ -27,15 +31,10 @@ module ActiveStorage
|
|
|
27
31
|
rescue => e
|
|
28
32
|
@variant_record&.update!(
|
|
29
33
|
state: "failed",
|
|
30
|
-
error: e.message,
|
|
34
|
+
error: e.message.to_s.truncate(16_000),
|
|
31
35
|
attempts: (@variant_record&.attempts || 0) + 1,
|
|
32
36
|
)
|
|
33
|
-
|
|
34
|
-
if @variant_record.nil? || @variant_record.attempts < max_retries
|
|
35
|
-
raise
|
|
36
|
-
else
|
|
37
|
-
logger.error "AsyncVariants: permanently failed after #{@variant_record.attempts} attempts: #{e.message}"
|
|
38
|
-
end
|
|
37
|
+
raise
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
private
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
module AsyncVariants
|
|
5
|
+
# Eagerly warm the digest-keyed Registry when a named variant is declared
|
|
6
|
+
# via has_X_attached's block:
|
|
7
|
+
#
|
|
8
|
+
# has_one_attached :avatar do |attachable|
|
|
9
|
+
# attachable.variant :thumb, resize_to_limit: [150, 150], format: "webp",
|
|
10
|
+
# transformer: Crucible, processing: "/spinner.svg"
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Without this, the Registry only warms when view-side code calls
|
|
14
|
+
# `attachment.variant(:thumb)` -- so a Puma worker that has never rendered
|
|
15
|
+
# a view for the variant misses on URL-reconstructed lookups.
|
|
16
|
+
#
|
|
17
|
+
# Caveat: registration uses the declared transformations. Rails applies a
|
|
18
|
+
# blob-dependent default `format:` at runtime via default_to. Variants that
|
|
19
|
+
# specify `format:` explicitly register the same digest the URL carries
|
|
20
|
+
# (cold-resolvable). Variants that rely on the format default still warm
|
|
21
|
+
# lazily on first view-side call against a blob of that content type.
|
|
22
|
+
module ReflectionExtension
|
|
23
|
+
def variant(name, transformations)
|
|
24
|
+
super
|
|
25
|
+
return unless transformations.is_a?(Hash)
|
|
26
|
+
# Mirror Blob#variant at runtime: wrap and default_to. The default_to
|
|
27
|
+
# call reorders hash keys (default key first) so the registered digest
|
|
28
|
+
# matches the URL-side digest for variants that declare an explicit
|
|
29
|
+
# `format:`. Variants without an explicit `format:` still warm lazily
|
|
30
|
+
# on first view-side call against a blob of that content type.
|
|
31
|
+
ActiveStorage::Variation.wrap(transformations).default_to(format: :png)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module ActiveStorage
|
|
6
|
+
module AsyncVariants
|
|
7
|
+
# Process-wide lookup of async_options keyed by Variation#digest.
|
|
8
|
+
#
|
|
9
|
+
# Populated lazily by VariationExtension#initialize whenever a Variation is
|
|
10
|
+
# constructed with :transformer in its async_options -- which happens on
|
|
11
|
+
# every view-side `attachment.variant(:name)` call. The redirect controller
|
|
12
|
+
# then resolves async_options by digest without scanning blob.attachments
|
|
13
|
+
# for a transformations-match.
|
|
14
|
+
#
|
|
15
|
+
# Cold-worker behavior: if a worker receives a redirect-URL request before
|
|
16
|
+
# any view-side call has registered the variant's digest, the lookup misses
|
|
17
|
+
# and resolution falls through to standard Rails (no leak, no spinner).
|
|
18
|
+
# Self-warms on subsequent view requests.
|
|
19
|
+
class Registry
|
|
20
|
+
MONITOR = Monitor.new
|
|
21
|
+
STORE = {}
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def register(digest, async_options)
|
|
25
|
+
MONITOR.synchronize { STORE[digest] = async_options }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](digest)
|
|
29
|
+
MONITOR.synchronize { STORE[digest] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear
|
|
33
|
+
MONITOR.synchronize { STORE.clear }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
module AsyncVariants
|
|
5
|
+
module RepresentationsRedirectControllerExtension
|
|
6
|
+
ASYNC_HEADER = "X-Async-Variant-State"
|
|
7
|
+
INTERCEPT_STATES = %w[pending processing failed].freeze
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
state = @representation.async_state
|
|
11
|
+
if INTERCEPT_STATES.include?(state)
|
|
12
|
+
fallback = @representation.url(disposition: params[:disposition])
|
|
13
|
+
if fallback.is_a?(String) && fallback.start_with?("/")
|
|
14
|
+
path = Rails.public_path.join(fallback.delete_prefix("/"))
|
|
15
|
+
if File.exist?(path)
|
|
16
|
+
response.set_header(ASYNC_HEADER, state)
|
|
17
|
+
response.set_header("Cache-Control", "no-store, private")
|
|
18
|
+
return send_file path, disposition: "inline"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
module AsyncVariants
|
|
5
|
+
module VariantRecordExtension
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
after_update_commit :touch_attached_records, if: :reached_terminal_state?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# processed and failed are terminal states: cache fragments built when
|
|
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.).
|
|
18
|
+
def reached_terminal_state?
|
|
19
|
+
saved_change_to_state? && %w[processed failed].include?(state)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def touch_attached_records
|
|
23
|
+
blob.attachments.includes(:record).each do |attachment|
|
|
24
|
+
attachment.record&.touch
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -3,16 +3,36 @@
|
|
|
3
3
|
module ActiveStorage
|
|
4
4
|
module AsyncVariants
|
|
5
5
|
module VariantWithRecordExtension
|
|
6
|
-
def
|
|
7
|
-
if
|
|
6
|
+
def processed
|
|
7
|
+
if blob.bucket_backed?
|
|
8
|
+
enqueue_processing unless processed? || processing?
|
|
9
|
+
self
|
|
10
|
+
else
|
|
8
11
|
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def url(...)
|
|
16
|
+
if blob.bucket_backed? && !processed?
|
|
17
|
+
fallback = active_fallback
|
|
18
|
+
case fallback
|
|
19
|
+
when :original then blob.url(...)
|
|
20
|
+
when :blank then nil
|
|
21
|
+
when Proc then fallback.call(blob)
|
|
22
|
+
when String then fallback
|
|
23
|
+
else super
|
|
24
|
+
end
|
|
9
25
|
else
|
|
10
|
-
|
|
26
|
+
super
|
|
11
27
|
end
|
|
12
28
|
end
|
|
13
29
|
|
|
14
|
-
def
|
|
15
|
-
|
|
30
|
+
def processed?
|
|
31
|
+
if blob.bucket_backed?
|
|
32
|
+
async_record&.state == "processed"
|
|
33
|
+
else
|
|
34
|
+
super
|
|
35
|
+
end
|
|
16
36
|
end
|
|
17
37
|
|
|
18
38
|
def processing?
|
|
@@ -31,29 +51,68 @@ module ActiveStorage
|
|
|
31
51
|
async_record&.error
|
|
32
52
|
end
|
|
33
53
|
|
|
54
|
+
def async_state
|
|
55
|
+
return nil unless blob.bucket_backed?
|
|
56
|
+
async_record&.state || "pending"
|
|
57
|
+
end
|
|
58
|
+
|
|
34
59
|
private
|
|
35
60
|
|
|
36
|
-
def
|
|
37
|
-
|
|
61
|
+
def resolved_async_options
|
|
62
|
+
@resolved_async_options ||=
|
|
63
|
+
variation.async_options.presence ||
|
|
64
|
+
ActiveStorage::AsyncVariants::Registry[variation.digest] ||
|
|
65
|
+
{}
|
|
38
66
|
end
|
|
39
67
|
|
|
40
|
-
def
|
|
41
|
-
|
|
68
|
+
def active_fallback
|
|
69
|
+
if failed?
|
|
70
|
+
resolved_async_options.fetch(:failed) { resolved_async_options[:processing] }
|
|
71
|
+
else
|
|
72
|
+
resolved_async_options[:processing]
|
|
73
|
+
end
|
|
42
74
|
end
|
|
43
75
|
|
|
44
|
-
def
|
|
45
|
-
|
|
76
|
+
def enqueue_processing
|
|
77
|
+
result = find_named_async_variant
|
|
78
|
+
return unless result
|
|
79
|
+
attachment, variant_name, _ = result
|
|
80
|
+
|
|
81
|
+
return if async_record
|
|
82
|
+
|
|
83
|
+
blob.variant_records.create!(
|
|
84
|
+
variation_digest: variation.digest,
|
|
85
|
+
state: "pending",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
ActiveStorage::AsyncVariants::ProcessJob.perform_later(
|
|
89
|
+
attachment.record, attachment.name, variant_name.to_s,
|
|
90
|
+
)
|
|
91
|
+
rescue ActiveRecord::RecordNotUnique
|
|
92
|
+
# another caller won the race; their job will handle processing
|
|
46
93
|
end
|
|
47
94
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
95
|
+
# Cold-path scan: only used by enqueue_processing, which needs the
|
|
96
|
+
# (attachment.record, attachment.name, variant_name) tuple to dispatch
|
|
97
|
+
# ProcessJob -- more than the digest registry stores. Hot-path URL
|
|
98
|
+
# resolution goes through Registry, not this method.
|
|
99
|
+
def find_named_async_variant
|
|
100
|
+
target = variation.transformations.to_json
|
|
101
|
+
|
|
102
|
+
blob.attachments.each do |attachment|
|
|
103
|
+
attachment.send(:named_variants).each do |name, _|
|
|
104
|
+
candidate = attachment.variant(name.to_sym)
|
|
105
|
+
if candidate.variation.transformations.to_json == target
|
|
106
|
+
return [attachment, name, candidate.variation.async_options] if candidate.variation.async_options[:processing].present?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
56
109
|
end
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def async_record
|
|
115
|
+
blob.variant_records.find_by(variation_digest: variation.digest)
|
|
57
116
|
end
|
|
58
117
|
end
|
|
59
118
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ActiveStorage
|
|
4
4
|
module AsyncVariants
|
|
5
5
|
module VariationExtension
|
|
6
|
-
ASYNC_KEYS = %i[
|
|
6
|
+
ASYNC_KEYS = %i[processing failed transformer].freeze
|
|
7
7
|
|
|
8
8
|
def initialize(transformations)
|
|
9
9
|
if transformations.is_a?(Hash)
|
|
@@ -13,6 +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
17
|
end
|
|
17
18
|
|
|
18
19
|
def async_options
|
|
@@ -2,36 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "async_variants/version"
|
|
4
4
|
require_relative "async_variants/transformer"
|
|
5
|
+
require_relative "async_variants/registry"
|
|
6
|
+
require_relative "async_variants/blob_extension"
|
|
5
7
|
require_relative "async_variants/variation_extension"
|
|
6
8
|
require_relative "async_variants/variant_with_record_extension"
|
|
9
|
+
require_relative "async_variants/variant_record_extension"
|
|
10
|
+
require_relative "async_variants/preview_extension"
|
|
7
11
|
require_relative "async_variants/attachment_extension"
|
|
12
|
+
require_relative "async_variants/reflection_extension"
|
|
8
13
|
require_relative "async_variants/process_job"
|
|
14
|
+
require_relative "async_variants/representations_redirect_controller_extension"
|
|
15
|
+
require_relative "async_variants/asset_tag_helper_extension"
|
|
9
16
|
|
|
10
17
|
module ActiveStorage
|
|
11
18
|
module AsyncVariants
|
|
19
|
+
mattr_accessor :cdn_host
|
|
20
|
+
|
|
12
21
|
class Engine < ::Rails::Engine
|
|
22
|
+
# :nocov:
|
|
23
|
+
initializer "active_storage_async_variants.assets" do |app|
|
|
24
|
+
if app.config.respond_to?(:assets)
|
|
25
|
+
app.config.assets.precompile += %w[active_storage_async_variants.js]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
# :nocov:
|
|
29
|
+
|
|
30
|
+
# Prepend the core model/reflection extensions before eager_load runs
|
|
31
|
+
# so that models' has_X_attached blocks (and the Variation.wrap calls
|
|
32
|
+
# they trigger via reflection.variant) go through our hooks. The
|
|
33
|
+
# :before_eager_load load_hook fires from the eager_load! initializer
|
|
34
|
+
# in finisher_hook, after all autoload paths have been set up but
|
|
35
|
+
# before any model class is loaded.
|
|
36
|
+
# :nocov:
|
|
37
|
+
ActiveSupport.on_load(:before_eager_load) do
|
|
38
|
+
ActiveStorage::AsyncVariants.prepend_model_extensions!
|
|
39
|
+
end
|
|
40
|
+
# :nocov:
|
|
41
|
+
|
|
13
42
|
config.after_initialize do
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
# Idempotent — covers eager_load=false (dev/test) where the
|
|
44
|
+
# :before_eager_load hook never fires. Models autoload lazily on
|
|
45
|
+
# demand, and we just need the extensions in place by the time
|
|
46
|
+
# the first one loads.
|
|
47
|
+
ActiveStorage::AsyncVariants.prepend_model_extensions!
|
|
48
|
+
|
|
49
|
+
ActiveStorage::Representations::RedirectController.prepend(
|
|
50
|
+
ActiveStorage::AsyncVariants::RepresentationsRedirectControllerExtension
|
|
19
51
|
)
|
|
20
|
-
|
|
21
|
-
ActiveStorage::AsyncVariants::
|
|
52
|
+
ActionView::Helpers::AssetTagHelper.prepend(
|
|
53
|
+
ActiveStorage::AsyncVariants::AssetTagHelperExtension
|
|
22
54
|
)
|
|
23
55
|
end
|
|
24
56
|
end
|
|
25
57
|
|
|
58
|
+
def self.prepend_model_extensions!
|
|
59
|
+
return if @model_extensions_prepended
|
|
60
|
+
@model_extensions_prepended = true
|
|
61
|
+
|
|
62
|
+
require "active_storage/reflection"
|
|
63
|
+
ActiveStorage::Reflection::HasAttachedReflection.prepend(
|
|
64
|
+
ActiveStorage::AsyncVariants::ReflectionExtension
|
|
65
|
+
)
|
|
66
|
+
ActiveStorage::Blob.prepend(
|
|
67
|
+
ActiveStorage::AsyncVariants::BlobExtension
|
|
68
|
+
)
|
|
69
|
+
ActiveStorage::Variation.prepend(
|
|
70
|
+
ActiveStorage::AsyncVariants::VariationExtension
|
|
71
|
+
)
|
|
72
|
+
ActiveStorage::VariantWithRecord.prepend(
|
|
73
|
+
ActiveStorage::AsyncVariants::VariantWithRecordExtension
|
|
74
|
+
)
|
|
75
|
+
ActiveStorage::VariantRecord.include(
|
|
76
|
+
ActiveStorage::AsyncVariants::VariantRecordExtension
|
|
77
|
+
)
|
|
78
|
+
ActiveStorage::Attachment.prepend(
|
|
79
|
+
ActiveStorage::AsyncVariants::AttachmentExtension
|
|
80
|
+
)
|
|
81
|
+
ActiveStorage::Preview.prepend(
|
|
82
|
+
ActiveStorage::AsyncVariants::PreviewExtension
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
26
86
|
def self.callback_token_for(variant_record)
|
|
27
87
|
ActiveStorage.verifier.generate(variant_record.id, purpose: :async_variant_callback)
|
|
28
88
|
end
|
|
29
89
|
|
|
30
90
|
def self.callback_url_for(variant_record)
|
|
91
|
+
url_options = ActiveStorage::Current.url_options || Rails.application.default_url_options
|
|
31
92
|
token = callback_token_for(variant_record)
|
|
32
93
|
Rails.application.routes.url_helpers.active_storage_async_variant_callback_url(
|
|
33
94
|
token: token,
|
|
34
|
-
**
|
|
95
|
+
**url_options,
|
|
35
96
|
)
|
|
36
97
|
end
|
|
37
98
|
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.1
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-29 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activestorage
|
|
@@ -38,18 +38,23 @@ files:
|
|
|
38
38
|
- LICENSE.txt
|
|
39
39
|
- README.md
|
|
40
40
|
- Rakefile
|
|
41
|
+
- app/assets/javascripts/active_storage_async_variants.js
|
|
41
42
|
- app/controllers/active_storage/async_variants/callbacks_controller.rb
|
|
42
43
|
- config/routes.rb
|
|
43
44
|
- gemfiles/rails_7.2.gemfile
|
|
44
|
-
- gemfiles/rails_7.2.gemfile.lock
|
|
45
45
|
- gemfiles/rails_8.0.gemfile
|
|
46
|
-
- gemfiles/rails_8.0.gemfile.lock
|
|
47
46
|
- gemfiles/rails_8.1.gemfile
|
|
48
|
-
- gemfiles/rails_8.1.gemfile.lock
|
|
49
47
|
- lib/active_storage/async_variants.rb
|
|
48
|
+
- lib/active_storage/async_variants/asset_tag_helper_extension.rb
|
|
50
49
|
- lib/active_storage/async_variants/attachment_extension.rb
|
|
50
|
+
- lib/active_storage/async_variants/blob_extension.rb
|
|
51
|
+
- lib/active_storage/async_variants/preview_extension.rb
|
|
51
52
|
- lib/active_storage/async_variants/process_job.rb
|
|
53
|
+
- lib/active_storage/async_variants/reflection_extension.rb
|
|
54
|
+
- lib/active_storage/async_variants/registry.rb
|
|
55
|
+
- lib/active_storage/async_variants/representations_redirect_controller_extension.rb
|
|
52
56
|
- lib/active_storage/async_variants/transformer.rb
|
|
57
|
+
- lib/active_storage/async_variants/variant_record_extension.rb
|
|
53
58
|
- lib/active_storage/async_variants/variant_with_record_extension.rb
|
|
54
59
|
- lib/active_storage/async_variants/variation_extension.rb
|
|
55
60
|
- lib/active_storage/async_variants/version.rb
|