defmodule Phoenix.LiveView.Diff do # The diff engine is responsible for tracking the rendering state. # Given that components are part of said state, they are also # handled here. @moduledoc false alias Phoenix.LiveView.{ Component, Comprehension, Lifecycle, Rendered, Utils } @components :c @static :s @keyed :k @keyed_count :kc @events :e @reply :r @title :t @template :p @stream :stream # We use this to track which components have been marked # for deletion. If the component is used after being marked, # it should not be deleted. @marked_for_deletion :marked_for_deletion @doc """ Returns the diff component state. """ def new_components(uuids \\ 1) do {_cid_to_component = %{}, _id_to_cid = %{}, uuids} end @doc """ Returns the diff fingerprint state. """ def new_fingerprints do {nil, %{}} end @doc """ Converts a diff into iodata. It only accepts a full render diff. """ def to_iodata(map, component_mapper \\ fn _cid, content -> content end) do to_iodata(map, Map.get(map, @components, %{}), Map.get(map, @template), component_mapper) |> elem(0) end defp to_iodata( %{@static => static, @keyed => keyed} = kc, components, template, mapper ) do template = template || kc[@template] if !keyed or keyed[@keyed_count] == 0 do {[], components} else Enum.map_reduce(0..(keyed[@keyed_count] - 1), components, fn index, components -> diff = Map.fetch!(keyed, index) to_iodata(Map.put(diff, @static, static), components, template, mapper) end) end end defp to_iodata(%{@static => static} = parts, components, template, mapper) do static = template_static(static, template) one_to_iodata(static, parts, 0, [], components, template, mapper) end defp to_iodata(cid, components, _template, mapper) when is_integer(cid) do # Resolve component pointers and update the component entries components = resolve_components_xrefs(cid, components) {iodata, components} = to_iodata(Map.fetch!(components, cid), components, nil, mapper) {mapper.(cid, iodata), components} end defp to_iodata(binary, components, _template, _mapper) when is_binary(binary) do {binary, components} end defp one_to_iodata([last], _parts, _counter, acc, components, _template, _mapper) do {Enum.reverse([last | acc]), components} end defp one_to_iodata([head | tail], parts, counter, acc, components, template, mapper) do {iodata, components} = to_iodata(Map.fetch!(parts, counter), components, template, mapper) one_to_iodata(tail, parts, counter + 1, [iodata, head | acc], components, template, mapper) end defp template_static(static, template) when is_integer(static), do: Map.fetch!(template, static) defp template_static(static, _template) when is_list(static), do: static defp resolve_components_xrefs(cid, components) do case components[cid] do %{@static => static} = diff when is_integer(static) -> static = abs(static) components = resolve_components_xrefs(static, components) Map.put(components, cid, deep_merge(components[static], Map.delete(diff, @static))) %{} -> components end end defp deep_merge(_original, %{@static => _} = extra), do: extra defp deep_merge(original, extra) do Map.merge(original, extra, fn _, %{} = original, %{} = extra -> deep_merge(original, extra) _, _original, extra -> extra end) end @doc """ Render information stored in private changed. """ def render_private(socket, diff) do diff |> maybe_put_reply(socket) |> maybe_put_events(socket) end @doc """ Renders a diff for the rendered struct in regards to the given socket. """ def render( socket, %Rendered{fingerprint: actual} = rendered, {expected, _}, {_, _, uuids} ) when expected != nil and expected != actual do render(socket, rendered, new_fingerprints(), new_components(uuids)) end def render(socket, %Rendered{} = rendered, prints, components) do {diff, prints, pending, components, template} = traverse(rendered, prints, %{}, components, {%{}, %{}}, true) # cid_to_component is used by maybe_reuse_static and it must be a copy before changes. # However, given traverse does not change cid_to_component, we can read it now. {cid_to_component, _, _} = components {cdiffs, components} = render_pending_components(socket, pending, cid_to_component, %{}, components) diff = diff |> maybe_add_template(template) |> maybe_put_title(socket) {diff, cdiffs} = extract_events({diff, cdiffs}) {maybe_put_cdiffs(diff, cdiffs), prints, components} end defp maybe_put_cdiffs(diff, cdiffs) when cdiffs == %{}, do: diff defp maybe_put_cdiffs(diff, cdiffs), do: Map.put(diff, @components, cdiffs) @doc """ Returns a diff containing only the events that have been pushed. """ def get_push_events_diff(socket) do if events = Utils.get_push_events(socket), do: %{@events => events} end defp maybe_put_title(diff, socket) do if Utils.changed?(socket.assigns, :page_title) do Map.put(diff, @title, socket.assigns.page_title) else diff end end defp maybe_put_events(diff, socket) do case Utils.get_push_events(socket) do [_ | _] = events -> Map.update(diff, @events, events, &(&1 ++ events)) [] -> diff end end defp extract_events({diff, component_diffs}) do case component_diffs do %{@events => component_events} -> {Map.update(diff, @events, component_events, &(&1 ++ component_events)), Map.delete(component_diffs, @events)} %{} -> {diff, component_diffs} end end defp maybe_put_reply(diff, socket) do case Utils.get_reply(socket) do nil -> diff reply -> Map.put(diff, @reply, reply) end end @doc """ Execute the `fun` with the component `cid` with the given `socket` as template. It returns the updated `cdiffs` and the updated `components` or `:error` if the component cid does not exist. """ def write_component(socket, cid, components, fun) when is_integer(cid) do # We need to extract the original cid_to_component for maybe_reuse_static later {cids, _, _} = components case cids do %{^cid => {component, id, assigns, private, prints}} -> {csocket, extra} = socket |> configure_socket_for_component(assigns, private) |> fun.(component) diff = render_private(csocket, %{}) {pending, cdiffs, components} = render_component(csocket, component, id, prints, cid, false, cids, %{}, components) {cdiffs, components} = render_pending_components(socket, pending, cids, cdiffs, components) {diff, cdiffs} = extract_events({diff, cdiffs}) {maybe_put_cdiffs(diff, cdiffs), components, extra} %{} -> :error end end @doc """ Execute the `fun` with the component `cid` with the given `socket` and returns the result. `:error` if the component cid does not exist. """ def read_component(socket, cid, components, fun) when is_integer(cid) do {cid_to_component, _id_to_cid, _} = components case cid_to_component do %{^cid => {component, _id, assigns, private, _prints}} -> socket |> configure_socket_for_component(assigns, private) |> fun.(component) %{} -> :error end end @doc """ Sends an update to a component. Like `write_component/5`, it will store the result under the `cid key in the `component_diffs` map. If the component exists, a `{diff, new_components}` tuple is returned. Otherwise, `:noop` is returned. The component is preloaded before the update callback is invoked. ## Example {diff, new_components} = Diff.update_component(socket, state.components, update) """ def update_component(socket, components, {ref, updated_assigns}) do case fetch_cid(ref, components) do {:ok, {cid, module}} -> updated_assigns = maybe_call_preload!(module, updated_assigns) {diff, new_components, :noop} = write_component(socket, cid, components, fn component_socket, component -> telemetry_metadata = %{ socket: socket, component: component, assigns_sockets: [{updated_assigns, component_socket}] } sockets = :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn -> {Utils.maybe_call_update!(component_socket, component, updated_assigns), telemetry_metadata} end) {sockets, :noop} end) {diff, new_components} :error -> :noop end end @doc """ Marks a component for deletion. It won't be deleted if the component is used meanwhile. """ def mark_for_deletion_component(cid, {cid_to_component, id_to_cid, uuids}) do cid_to_component = case cid_to_component do %{^cid => {component, id, assigns, private, prints}} -> private = Map.put(private, @marked_for_deletion, true) Map.put(cid_to_component, cid, {component, id, assigns, private, prints}) %{} -> cid_to_component end {cid_to_component, id_to_cid, uuids} end @doc """ Deletes a component by `cid` if it has not been used meanwhile. """ def delete_component(cid, {cid_to_component, id_to_cid, uuids}) do case cid_to_component do %{^cid => {component, id, _, %{@marked_for_deletion => true}, _}} -> id_to_cid = case id_to_cid do %{^component => inner} -> case Map.delete(inner, id) do inner when inner == %{} -> Map.delete(id_to_cid, component) inner -> Map.put(id_to_cid, component, inner) end %{} -> id_to_cid end {[cid], {Map.delete(cid_to_component, cid), id_to_cid, uuids}} _ -> {[], {cid_to_component, id_to_cid, uuids}} end end @doc """ Converts a component to a rendered struct. """ def component_to_rendered(socket, component, assigns, mount_assigns) when is_map(assigns) do component_socket = mount_component(socket, component, mount_assigns) assigns = maybe_call_preload!(component, assigns) telemetry_metadata = %{ socket: socket, component: component, assigns_sockets: [{assigns, component_socket}] } :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn -> result = component_socket |> Utils.maybe_call_update!(component, assigns) |> component_to_rendered(component, assigns[:id]) {result, telemetry_metadata} end) end defp component_to_rendered(socket, component, id) do rendered = Phoenix.LiveView.Renderer.to_rendered(socket, component) if rendered.root != true and id != nil do reason = case rendered.root do nil -> "Stateful components must return a HEEx template (~H sigil or .heex extension)" false -> "Stateful components must have a single static HTML tag at the root" end raise ArgumentError, "error on #{inspect(component)}.render/1 with id of #{inspect(id)}. #{reason}" end rendered end ## Traversal defp traverse( %Rendered{fingerprint: fingerprint} = rendered, {fingerprint, children}, pending, components, template, changed? ) do {_counter, diff, children, pending, components, template} = traverse_dynamic( invoke_dynamic(rendered, changed?), children, pending, components, template, changed? ) {diff, {fingerprint, children}, pending, components, template} end defp traverse( %Rendered{fingerprint: fingerprint, static: static} = rendered, _, pending, components, template, changed? ) do {_counter, diff, children, pending, components, template} = traverse_dynamic( invoke_dynamic(rendered, false), %{}, pending, components, template, changed? ) diff = if rendered.root, do: Map.put(diff, :r, 1), else: diff {diff, template} = maybe_share_template(diff, fingerprint, static, template) {diff, {fingerprint, children}, pending, components, template} end defp traverse( %Component{} = component, _fingerprints_tree, pending, components, template, _changed? ) do {cid, pending, components} = traverse_component(component, pending, components) {cid, nil, pending, components, template} end defp traverse( %Comprehension{ fingerprint: fingerprint, entries: entries, stream: stream, has_key?: has_key? }, {fingerprint, previous_prints}, pending, components, template, changed? ) do if template do {keyed, keyed_prints, pending, components, template} = traverse_keyed( entries, previous_prints, pending, components, template, changed?, stream != nil, has_key? ) diff = %{} |> maybe_add_keyed(keyed) |> maybe_add_stream(stream) {diff, {fingerprint, keyed_prints}, pending, components, template} else {keyed, keyed_prints, pending, components, template} = traverse_keyed( entries, previous_prints, pending, components, {%{}, %{}}, changed?, stream != nil, has_key? ) diff = %{} |> maybe_add_keyed(keyed) |> maybe_add_stream(stream) |> maybe_add_template(template) {diff, {fingerprint, keyed_prints}, pending, components, nil} end end defp traverse( %Comprehension{entries: [], stream: nil}, _, pending, components, template, _changed? ) do # The comprehension has no elements and it was not rendered yet, # so we can skip it as long as it doesn't have a stream. {"", nil, pending, components, template} end defp traverse( %Comprehension{ static: static, fingerprint: fingerprint, entries: entries, stream: stream, has_key?: has_key? }, _, pending, components, template, changed? ) do if template do {keyed, keyed_prints, pending, components, template} = traverse_keyed( entries, %{}, pending, components, template, changed?, stream != nil, has_key? ) {diff, template} = %{@keyed => keyed} |> maybe_add_stream(stream) |> maybe_share_template(fingerprint, static, template) {diff, {fingerprint, keyed_prints}, pending, components, template} else {keyed, keyed_prints, pending, components, template} = traverse_keyed( entries, %{}, pending, components, {%{}, %{}}, changed?, stream != nil, has_key? ) diff = %{@static => static, @keyed => keyed} |> maybe_add_stream(stream) |> maybe_add_template(template) {diff, {fingerprint, keyed_prints}, pending, components, nil} end end defp traverse(nil, fingerprint_tree, pending, components, template, _changed?) do {nil, fingerprint_tree, pending, components, template} end defp traverse(iodata, _, pending, components, template, _changed?) do {IO.iodata_to_binary(iodata), nil, pending, components, template} end defp invoke_dynamic(%Rendered{caller: :not_available, dynamic: dynamic}, changed?) do dynamic.(changed?) end defp invoke_dynamic(%Rendered{caller: caller, dynamic: dynamic}, changed?) do try do dynamic.(changed?) rescue e -> {mod, {function, arity}, file, line} = caller entry = {mod, function, arity, file: String.to_charlist(file), line: line} reraise e, inject_stacktrace(__STACKTRACE__, entry) end end defp inject_stacktrace([{__MODULE__, :invoke_dynamic, 2, _} | stacktrace], entry) do [entry | Enum.drop_while(stacktrace, &(elem(&1, 0) == __MODULE__))] end defp inject_stacktrace([head | tail], entry) do [head | inject_stacktrace(tail, entry)] end defp inject_stacktrace([], entry) do [entry] end defp traverse_dynamic(dynamic, children, pending, components, template, changed?) do Enum.reduce(dynamic, {0, %{}, children, pending, components, template}, fn entry, {counter, diff, children, pending, components, template} -> child = Map.get(children, counter) {serialized, child_fingerprint, pending, components, template} = traverse(entry, child, pending, components, template, changed?) # If serialized is nil, it means no changes. # If it is an empty map, then it means it is a rendered struct # that did not change, so we don't have to emit it either. diff = if serialized != nil and serialized != %{} do Map.put(diff, counter, serialized) else diff end children = if child_fingerprint do Map.put(children, counter, child_fingerprint) else Map.delete(children, counter) end {counter + 1, diff, children, pending, components, template} end) end defp traverse_keyed( entries, previous_prints, pending, components, template, changed?, stream?, has_key? ) do diff = %{} new_prints = %{} {{diff, count, new_prints, pending, components, template}, _seen_keys} = Enum.reduce( entries, {{diff, 0, new_prints, pending, components, template}, MapSet.new()}, fn {key, vars, render}, {{_diff, index, _new_prints, _pending, _components, _template} = acc, seen_keys} -> {key, seen_keys} = cond do not has_key? -> # no need to check for duplicates if we use the index {index, seen_keys} MapSet.member?(seen_keys, key) -> raise "found duplicate key #{inspect(key)} in comprehension" true -> {key, MapSet.put(seen_keys, key)} end {process_keyed({key, vars, render}, previous_prints, changed?, stream?, acc), seen_keys} end ) # we don't need to send the diff if nothing changed; if diff == %{} and count > 0 and count == map_size(previous_prints) do {nil, new_prints, pending, components, template} else {Map.put(diff, @keyed_count, count), new_prints, pending, components, template} end end # it's an existing entry and we are change tracking defp process_keyed({key, new_vars, render}, previous_prints, true = changed?, stream?, acc) when is_map_key(previous_prints, key) and not stream? do {diff, index, new_prints, pending, components, template} = acc %{vars: previous_vars, index: previous_index, child_prints: child_prints} = Map.fetch!(previous_prints, key) vars_changed = Enum.reduce(new_vars, Map.put(previous_vars, :__changed__, %{}), fn {key, value}, acc -> Phoenix.Component.assign(acc, key, value) end) |> Map.fetch!(:__changed__) {_counter, child_diff, child_prints, pending, components, template} = traverse_dynamic( render.(vars_changed, changed?), child_prints, pending, components, template, changed? ) new_prints = Map.put(new_prints, key, %{index: index, vars: new_vars, child_prints: child_prints}) # if the diff is empty, we need to check if the item moved if child_diff == %{} or child_diff == nil do # check if the entry moved, then annotate it with the previous index diff = if previous_index != index, do: Map.put(diff, index, previous_index), else: diff {diff, index + 1, new_prints, pending, components, template} else child_diff = if previous_index != index do [previous_index, child_diff] else child_diff end {Map.put(diff, index, child_diff), index + 1, new_prints, pending, components, template} end end # it's a new entry defp process_keyed({key, vars, render}, _previous_prints, _changed?, stream?, acc) do {diff, index, new_prints, pending, components, template} = acc {_counter, child_diff, child_prints, pending, components, template} = traverse_dynamic( render.(%{}, false), %{}, pending, components, template, # we need to disable change-tracking to force a full render, # even if some parts of the template might not have changed themselves false ) # if this is a stream, we don't store any fingerprints new_prints = if stream? do {%{}, nil} else Map.put(new_prints, key, %{index: index, vars: vars, child_prints: child_prints}) end diff = Map.put(diff, index, child_diff) {diff, index + 1, new_prints, pending, components, template} end defp maybe_share_template(map, fingerprint, static, {print_to_pos, pos_to_static}) do case print_to_pos do %{^fingerprint => pos} -> {Map.put(map, @static, pos), {print_to_pos, pos_to_static}} %{} -> pos = map_size(pos_to_static) pos_to_static = Map.put(pos_to_static, pos, static) print_to_pos = Map.put(print_to_pos, fingerprint, pos) {Map.put(map, @static, pos), {print_to_pos, pos_to_static}} end end defp maybe_share_template(map, _fingerprint, static, nil) do {Map.put(map, @static, static), nil} end defp maybe_add_template(map, {_, template}) when template != %{}, do: Map.put(map, @template, template) defp maybe_add_template(map, _new_template), do: map defp maybe_add_stream(diff, nil = _stream), do: diff defp maybe_add_stream(diff, stream), do: Map.put(diff, @stream, stream) defp maybe_add_keyed(diff, nil = _keyed), do: diff defp maybe_add_keyed(diff, keyed), do: Map.put(diff, @keyed, keyed) ## Stateful components helpers defp traverse_component( %Component{id: id, assigns: assigns, component: component}, pending, {cid_to_component, id_to_cid, uuids} ) do {cid, new?, components} = case id_to_cid do %{^component => %{^id => cid}} -> {cid, false, {cid_to_component, id_to_cid, uuids}} %{} -> {uuids, true, {cid_to_component, id_to_cid, uuids + 1}} end entry = {cid, id, new?, assigns} pending = Map.update(pending, component, [entry], &[entry | &1]) {cid, pending, components} end ## Component rendering defp render_pending_components(socket, pending, cids, diffs, components) do render_pending_components(socket, pending, %{}, cids, diffs, components) end defp render_pending_components(_, pending, _seen_ids, _cids, diffs, components) when map_size(pending) == 0 do {diffs, components} end defp render_pending_components(socket, pending, seen_ids, cids, diffs, components) do acc = {{%{}, diffs, components}, seen_ids} {{pending, diffs, components}, seen_ids} = Enum.reduce(pending, acc, fn {component, entries}, acc -> {{pending, diffs, components}, seen_ids} = acc update_many? = function_exported?(component, :update_many, 1) entries = maybe_preload_components(component, Enum.reverse(entries)) {assigns_sockets, metadata, components, seen_ids} = Enum.reduce(entries, {[], [], components, seen_ids}, fn {cid, id, new?, new_assigns}, {assigns_sockets, metadata, components, seen_ids} -> if Map.has_key?(seen_ids, [component | id]) do raise "found duplicate ID #{inspect(id)} " <> "for component #{inspect(component)} when rendering template" end {socket, components, prints} = case cids do %{^cid => {_component, _id, assigns, private, prints}} -> {private, components} = unmark_for_deletion(private, components) {configure_socket_for_component(socket, assigns, private), components, prints} %{} -> myself_assigns = %{myself: %Phoenix.LiveComponent.CID{cid: cid}} {mount_component(socket, component, myself_assigns), put_cid(components, component, id, cid), new_fingerprints()} end assigns_sockets = [{new_assigns, socket} | assigns_sockets] metadata = [{cid, id, prints, new?} | metadata] seen_ids = Map.put(seen_ids, [component | id], true) {assigns_sockets, metadata, components, seen_ids} end) assigns_sockets = Enum.reverse(assigns_sockets) telemetry_metadata = %{ socket: socket, component: component, assigns_sockets: assigns_sockets } sockets = :telemetry.span([:phoenix, :live_component, :update], telemetry_metadata, fn -> sockets = if update_many? do component.update_many(assigns_sockets) else Enum.map(assigns_sockets, fn {assigns, socket} -> Utils.maybe_call_update!(socket, component, assigns) end) end {sockets, Map.put(telemetry_metadata, :sockets, sockets)} end) metadata = Enum.reverse(metadata) triplet = zip_components(sockets, metadata, component, cids, {pending, diffs, components}) {triplet, seen_ids} end) render_pending_components(socket, pending, seen_ids, cids, diffs, components) end defp zip_components( [%{__struct__: Phoenix.LiveView.Socket} = socket | sockets], [{cid, id, prints, new?} | metadata], component, cids, {pending, diffs, components} ) do diffs = maybe_put_events(diffs, socket) {new_pending, diffs, components} = render_component(socket, component, id, prints, cid, new?, cids, diffs, components) pending = Map.merge(pending, new_pending, fn _, v1, v2 -> v2 ++ v1 end) zip_components(sockets, metadata, component, cids, {pending, diffs, components}) end defp zip_components([], [], _component, _cids, acc) do acc end defp zip_components(_sockets, _metadata, component, _cids, _acc) do raise "#{inspect(component)}.update_many/1 must return a list of Phoenix.LiveView.Socket " <> "of the same length as the input list, got mismatched return type" end defp maybe_preload_components(component, entries) do if function_exported?(component, :preload, 1) do IO.warn("#{inspect(component)}.preload/1 is deprecated, use update_many/1 instead") list_of_assigns = Enum.map(entries, fn {_cid, _id, _new?, new_assigns} -> new_assigns end) result = component.preload(list_of_assigns) zip_preloads(result, entries, component, result) else entries end end defp maybe_call_preload!(module, assigns) do if function_exported?(module, :preload, 1) do IO.warn("#{inspect(module)}.preload/1 is deprecated, use update_many/1 instead") [new_assigns] = module.preload([assigns]) new_assigns else assigns end end defp zip_preloads([new_assigns | assigns], [{cid, id, new?, _} | entries], component, preloaded) when is_map(new_assigns) do [{cid, id, new?, new_assigns} | zip_preloads(assigns, entries, component, preloaded)] end defp zip_preloads([], [], _component, _preloaded) do [] end defp zip_preloads(_, _, component, preloaded) do raise ArgumentError, "expected #{inspect(component)}.preload/1 to return a list of maps of the same length " <> "as the list of assigns given, got: #{inspect(preloaded)}" end defp render_component(socket, component, id, prints, cid, new?, cids, diffs, components) do changed? = new? or Utils.changed?(socket) {socket, prints, pending, diff, components} = if changed? do rendered = component_to_rendered(socket, component, id) {changed?, linked_cid, prints} = maybe_reuse_static(rendered, component, prints, cids, components) {diff, prints, pending, components, nil} = traverse(rendered, prints, %{}, components, nil, changed?) children_cids = for {_component, list} <- pending, entry <- list, do: elem(entry, 0) diff = if linked_cid, do: Map.put(diff, @static, linked_cid), else: diff socket = put_in(socket.private.children_cids, children_cids) |> Lifecycle.after_render() |> Utils.clear_changed() {socket, prints, pending, diff, components} else {socket, prints, %{}, %{}, components} end diffs = if diff != %{} or new? do Map.put(diffs, cid, diff) else diffs end dump = socket |> Utils.clear_temp() |> dump_component(component, id, prints) {cid_to_component, id_to_cid, uuids} = components cid_to_component = Map.put(cid_to_component, cid, dump) {pending, diffs, {cid_to_component, id_to_cid, uuids}} end defp unmark_for_deletion(private, {cid_to_component, id_to_cid, uuids}) do {private, cid_to_component} = do_unmark_for_deletion(private, cid_to_component) {private, {cid_to_component, id_to_cid, uuids}} end defp do_unmark_for_deletion(private, cids) do {marked?, private} = Map.pop(private, @marked_for_deletion, false) cids = if marked? do Enum.reduce(private.children_cids, cids, fn cid, cids -> case cids do %{^cid => {component, id, assigns, private, prints}} -> {private, cids} = do_unmark_for_deletion(private, cids) Map.put(cids, cid, {component, id, assigns, private, prints}) %{} -> cids end end) else cids end {private, cids} end # 32 is one bucket from large maps @attempts 32 # If the component is new or is getting a new static root, we search if another # component has the same tree root. If so, we will point to the whole existing # component tree but say all entries require a full render. # # When looking up for an existing component, we first look into the tree from the # previous render, then we look at the new render. This is to avoid using a tree # that will be changed before it is sent to the client. # # We don't want to traverse all of the components, so we will try it @attempts times. defp maybe_reuse_static(rendered, component, prints, old_cids, components) do {new_cids, id_to_cid, _uuids} = components {current_print, _} = prints %{fingerprint: print} = rendered with true <- current_print != print, iterator = :maps.iterator(Map.fetch!(id_to_cid, component)), {cid, existing_prints} <- find_same_component_print(print, iterator, old_cids, new_cids, @attempts) do {false, cid, existing_prints} else _ -> {true, nil, prints} end end defp find_same_component_print(_print, _iterator, _old_cids, _new_cids, 0), do: :none defp find_same_component_print(print, iterator, old_cids, new_cids, attempts) do case :maps.next(iterator) do {_, cid, iterator} -> case old_cids do # if a component is marked for deletion, we cannot share its statics since it may be removed %{^cid => {_, _, _, %{@marked_for_deletion => true}, {^print, _} = _tree}} -> find_same_component_print(print, iterator, old_cids, new_cids, attempts - 1) %{^cid => {_, _, _, _private, {^print, _} = tree}} -> {-cid, tree} %{} -> case new_cids do %{^cid => {_, _, _, _, {^print, _} = tree}} -> {cid, tree} %{} -> find_same_component_print(print, iterator, old_cids, new_cids, attempts - 1) end end :none -> :none end end defp put_cid({id_to_components, id_to_cid, uuids}, component, id, cid) do inner = Map.get(id_to_cid, component, %{}) {id_to_components, Map.put(id_to_cid, component, Map.put(inner, id, cid)), uuids} end defp fetch_cid( %Phoenix.LiveComponent.CID{cid: cid}, {cid_to_components, _id_to_cid, _} = _components ) do case cid_to_components do %{^cid => {component, _id, _assigns, _private, _fingerprints}} -> {:ok, {cid, component}} %{} -> :error end end defp fetch_cid({component, id}, {_cid_to_components, id_to_cid, _} = _components) do case id_to_cid do %{^component => %{^id => cid}} -> {:ok, {cid, component}} %{} -> :error end end defp mount_component(socket, component, assigns) do private = socket.private |> Map.take([:conn_session, :root_view]) |> Map.put(:live_temp, %{}) |> Map.put(:children_cids, []) |> Map.put(:lifecycle, %Phoenix.LiveView.Lifecycle{}) socket = configure_socket_for_component(socket, assigns, private) |> Utils.assign(:flash, %{}) Utils.maybe_call_live_component_mount!(socket, component) end defp configure_socket_for_component(socket, assigns, private) do %{ socket | assigns: Map.put(assigns, :__changed__, %{}), private: private, redirected: nil } end defp dump_component(socket, component, id, prints) do {component, id, socket.assigns, socket.private, prints} end end