defmodule Absinthe.Subscription.PipelineSerializer do
@moduledoc """
Serializer responsible for packing and unpacking pipeline stored in the Elixir registry.
The purpose of this logic is saving memory by deduplicating repeating options - (ETS
backed registry stores them flat in the memory).
"""
alias Absinthe.{Phase, Pipeline}
@type options_label :: {:options, non_neg_integer()}
@type packed_phase_config :: Phase.t() | {Phase.t(), options_label()}
@type options_map :: %{options_label() => Keyword.t()}
@type packed_pipeline :: {:packed, [packed_phase_config()], options_map()}
@spec pack(Pipeline.t()) :: packed_pipeline()
def pack(pipeline) do
{packed_pipeline, options_reverse_map} =
pipeline
|> List.flatten()
|> Enum.map_reduce(%{}, &maybe_pack_phase/2)
options_map = Map.new(options_reverse_map, fn {options, label} -> {label, options} end)
{:packed, packed_pipeline, options_map}
end
@spec unpack(Pipeline.t() | packed_pipeline()) :: Pipeline.t()
def unpack({:packed, pipeline, options_map}) do
Enum.map(pipeline, fn
{phase, {:options, _n} = options_label} ->
{phase, Map.fetch!(options_map, options_label)}
phase ->
phase
end)
end
def unpack([_ | _] = pipeline) do
pipeline
end
defp maybe_pack_phase({phase, options}, options_reverse_map) do
if Map.has_key?(options_reverse_map, options) do
options_label = options_reverse_map[options]
{{phase, options_label}, options_reverse_map}
else
new_index = map_size(options_reverse_map)
options_label = {:options, new_index}
options_reverse_map = Map.put(options_reverse_map, options, options_label)
{{phase, options_label}, options_reverse_map}
end
end
defp maybe_pack_phase(phase, options_reverse_map) do
{phase, options_reverse_map}
end
end