Module: Axn::Internal::AsyncSerialization

Defined in:
lib/axn/internal/async_serialization.rb

Overview

Dispatcher for async argument serialization. See lib/axn/internal/async_serialization.rb header comment / docs/superpowers/plans for the design.

Constant Summary collapse

GENERIC_HINT =
"Async args must be JSON-native values (String, Integer, Float, true/false, nil, " \
"Array/Hash of those) or GlobalID-able objects (e.g. ActiveRecord records, " \
"ActiveStorage attachments)."

Class Method Summary collapse

Class Method Details

.assert_serializable!(params) ⇒ Object

Validate that every arg will serialize for async, raising a field-aware UnserializableArgument otherwise — without keeping the result. Lets an adapter that serializes natively (the ActiveJob adapter, via perform_later) surface the same enqueue-time contract as the Sidekiq path instead of leaking ActiveJob::SerializationError. The throwaway serialize is a cold-path cost; the adapter still serializes the real payload exactly once (no double-encoding).



50
51
52
53
# File 'lib/axn/internal/async_serialization.rb', line 50

def assert_serializable!(params)
  serialize(params)
  nil
end

.deserialize(params) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/axn/internal/async_serialization.rb', line 26

def deserialize(params)
  return {} if params.nil? || params.empty?

  # A fallback payload tags GlobalID args with the `_as_global_id` key suffix, and
  # GlobalIdSerialization can decode that whether or not ActiveJob is loaded — so route
  # a fallback-encoded payload there by its marker, covering an enqueue-without-ActiveJob
  # / perform-with-ActiveJob mismatch. Otherwise use the ActiveJob decoder only when
  # ActiveJob is actually loaded (its `_aj_*` format is undecodable without it anyway);
  # a marker-free JSON-native payload decodes identically either way. We deliberately do
  # NOT sniff for `_aj_*` keys to force the ActiveJob decoder: user data can legitimately
  # contain `_aj_`-prefixed keys, and forcing ActiveJob where it isn't loaded would raise
  # a NameError on otherwise-valid fallback data.
  return Axn::Internal::GlobalIdSerialization.deserialize(params) if _fallback_encoded?(params)
  return _deserialize_via_active_job(params) if _active_job_available?

  Axn::Internal::GlobalIdSerialization.deserialize(params)
end

.prepare_nested_payload(value) ⇒ Object

A value that rides nested inside another payload which an adapter will itself serialize needs path-specific handling. On the ActiveJob path the adapter's serializer recurses into nested hashes AND is not idempotent over its own tags, so pre-serializing here would double-encode and raise — pass it through untouched. On the fallback path the serializer is top-level-only and can't reach nested objects, so flatten them now; the resulting all-string hash is left untouched by the adapter's top-level pass.



62
63
64
# File 'lib/axn/internal/async_serialization.rb', line 62

def prepare_nested_payload(value)
  _active_job_available? ? value : serialize(value)
end

.restore_nested_payload(value) ⇒ Object

Inverse of prepare_nested_payload. Decide what to decode from the payload itself, not this worker's ActiveJob state, so a fallback enqueue performed on an ActiveJob worker (or vice versa) still restores. ActiveJob-restored static_args arrive with their original (symbol) keys and live objects already rebuilt — re-running deserialize on those makes ActiveJob reject the live records ("can only deserialize primitive arguments"). Fallback-flattened static_args arrive with String keys (and any _as_global_id suffixes) still needing a decode. So decode only the String-keyed form; deserialize is itself format-aware, so it picks the right decoder for the markers.



74
75
76
77
78
# File 'lib/axn/internal/async_serialization.rb', line 74

def restore_nested_payload(value)
  return value unless value.is_a?(Hash) && value.keys.any? { |k| k.is_a?(String) }

  deserialize(value)
end

.serialize(params) ⇒ Object



18
19
20
21
22
23
24
# File 'lib/axn/internal/async_serialization.rb', line 18

def serialize(params)
  return {} if params.nil? || params.empty?
  return _serialize_via_active_job(params) if _active_job_available?

  params.each { |key, value| _assert_fallback_serializable!(key, value) }
  Axn::Internal::GlobalIdSerialization.serialize(params)
end