A2UICore.Resolution.NodeResolver (A2UICore v0.1.1)

Copy Markdown View Source

Resolves a surface's flat component dictionary into a tree of A2UICore.Resolution.Nodes.

Child references become nodes, collection templates become one node per item (each in its item's data scope), components that have not arrived yet become :pending placeholders that are replaced when their definition does, and a component referenced by one of its own descendants is cut with a :cyclic placeholder.

The resolver is plain data. It does not observe anything: after the surface changed, hand the new surface to update/2 and read what happened from the notifications it returns.

{resolver, notifications} = NodeResolver.new(surface)
surface = Surface.put_components(surface, components)
{resolver, notifications} = NodeResolver.update(resolver, surface)

Identity

A position is identified by the edge that leads to it (the parent position, the property, and the component and data path it points at). While the edge keeps leading to the same kind of node, the node keeps its ref; only its props change. A node is replaced — and the old one destroyed — when the component behind the edge appears, goes away or changes type, when a sibling ordinal shifts, or when its parent is destroyed.

Notifications

  • {:node_created, ref} — a new position is mounted;
  • {:node_updated, ref} — a node that was already mounted now has different props (reported once per update/2, whatever number of properties changed);
  • {:node_destroyed, ref} — a mounted node is gone; reported once;
  • {:error | :warning, payload} — a diagnostic (an unknown component type, a cyclic reference, a binding that does not resolve), reported when the condition appears and again only after it went away.

write/4 and invoke/5 are the interaction half: they say which path a write reaches and which events and calls an action produces, from the surface as it is now.

Agent functions

From v1.0 a function call that the surface's catalogs cannot run locally is the agent's (see Agent functions). The resolver keeps what it knows of those calls in agent_calls. When a property needs a call it has not seen, update/2 marks the call pending and reports {:agent_call, %{key: key, call: call}}: the renderer sends call to the agent and later hands the outcome to settle_agent_call/3, then calls update/2 again. Until then the property is nil and its node is busy. Settled calls are kept for the properties and actions that use them again; past 256 calls, the settled ones no property uses are forgotten.

Summary

Functions

Number of mounted nodes, placeholders included.

Destroys every node and stops the resolver; further updates do nothing. Idempotent.

Invokes the action in property of node ref, evaluating its dynamic parts against surface as it is now.

Resolves surface from scratch.

The mounted node with ref, or nil.

The mounted nodes by position: "root", "root/child", "root/children/0", "root/tabs/1/child". Positions describe where a node sits in the tree; they are not identities.

The root node, or nil while the root component is missing.

Records the outcome of the agent function call key (from an {:agent_call, %{key: key}} notification): {:ok, value} or {:error, payload}. Call update/2 afterwards to resolve the properties that use it. A call the resolver no longer knows is ignored.

Marks the agent function call key as sent, so that properties that use it wait for its outcome instead of reporting it again; for the calls an action's interactions ask for (invoke/5).

Reconciles the tree with surface's current components and data.

What a write to property of node ref changes: {:ok, path, value} when the property is a binding to a data path, an error otherwise. Apply it with A2UICore.State.Surface.set_data/3 and update/2.

Types

interaction()

(since 0.1.0)
@type interaction() ::
  {:action_event, map()}
  | {:error | :warning, map()}
  | {:write | :messages | :agent_call, map()}

notification()

(since 0.1.0)
@type notification() ::
  {:node_created, pos_integer()}
  | {:node_updated, pos_integer()}
  | {:node_destroyed, pos_integer()}
  | {:error | :warning, map()}
  | {:agent_call, map()}

t()

(since 0.1.0)
@type t() :: %A2UICore.Resolution.NodeResolver{
  agent_calls: %{
    optional(term()) => A2UICore.Resolution.DataContext.agent_call_state()
  },
  disposed?: boolean(),
  edges: %{optional(String.t()) => pos_integer()},
  next_ref: pos_integer(),
  nodes: %{optional(pos_integer()) => A2UICore.Resolution.Node.t()},
  reported: MapSet.t(),
  root: pos_integer() | nil,
  surface_id: String.t() | nil
}

Functions

count(node_resolver)

(since 0.1.0)
@spec count(t()) :: non_neg_integer()

Number of mounted nodes, placeholders included.

dispose(resolver)

(since 0.1.0)
@spec dispose(t()) :: {t(), [notification()]}

Destroys every node and stops the resolver; further updates do nothing. Idempotent.

invoke(resolver, surface, ref, property, now \\ DateTime.utc_now())

(since 0.1.0)
@spec invoke(t(), A2UICore.State.Surface.t(), pos_integer(), String.t(), DateTime.t()) ::
  {:ok, [interaction()]} | {:error, A2UICore.Error.t()}

Invokes the action in property of node ref, evaluating its dynamic parts against surface as it is now.

An event action yields {:action_event, event} (see A2UICore.State.Surface.dispatch_action/4); a function-call action runs the catalog function and yields no event. Resolution problems come back as {:error, payload} / {:warning, payload} entries, and what the functions ask for as {:write, ...} / {:messages, ...} entries for the renderer to apply (see A2UICore.Resolution.DataContext).

new(surface)

(since 0.1.0)
@spec new(A2UICore.State.Surface.t()) :: {t(), [notification()]}

Resolves surface from scratch.

node(node_resolver, ref)

(since 0.1.0)
@spec node(t(), pos_integer()) :: A2UICore.Resolution.Node.t() | nil

The mounted node with ref, or nil.

positions(resolver)

(since 0.1.0)
@spec positions(t()) :: %{optional(String.t()) => A2UICore.Resolution.Node.t()}

The mounted nodes by position: "root", "root/child", "root/children/0", "root/tabs/1/child". Positions describe where a node sits in the tree; they are not identities.

root(node_resolver)

(since 0.1.0)
@spec root(t()) :: A2UICore.Resolution.Node.t() | nil

The root node, or nil while the root component is missing.

settle_agent_call(resolver, key, outcome)

(since 0.1.0)
@spec settle_agent_call(t(), term(), {:ok, term()} | {:error, map()}) :: t()

Records the outcome of the agent function call key (from an {:agent_call, %{key: key}} notification): {:ok, value} or {:error, payload}. Call update/2 afterwards to resolve the properties that use it. A call the resolver no longer knows is ignored.

track_agent_call(resolver, key)

(since 0.1.0)
@spec track_agent_call(t(), term()) :: t()

Marks the agent function call key as sent, so that properties that use it wait for its outcome instead of reporting it again; for the calls an action's interactions ask for (invoke/5).

update(resolver, surface)

(since 0.1.0)
@spec update(t(), A2UICore.State.Surface.t()) :: {t(), [notification()]}

Reconciles the tree with surface's current components and data.

Returns the new resolver and the notifications, in the order things were found (creations and updates, then destructions).

write(resolver, ref, property, value)

(since 0.1.0)
@spec write(t(), pos_integer(), String.t(), term()) ::
  {:ok, String.t(), term()} | {:error, A2UICore.Error.t()}

What a write to property of node ref changes: {:ok, path, value} when the property is a binding to a data path, an error otherwise. Apply it with A2UICore.State.Surface.set_data/3 and update/2.