# `A2UICore.State.DataModel`
[🔗](https://github.com/a2ui-elixir/a2ui/blob/v0.1.1/umbrella/apps/a2ui_core/lib/a2ui_core/state/data_model.ex#L1)

The reactive JSON data store behind a surface — an immutable port of
the reference `DataModel` (RFC 6901 pointers, auto-vivifying writes,
cascading change notifications).

The model is plain data: every write returns the new model together
with the notifications it caused, and the caller (a surface, a
LiveView, a test) decides how to deliver them. There are no processes
and no callbacks.

## Reads

`get/2` returns `{:ok, value}`, with `nil` for an absent path — an
absent path and an explicit `null` are indistinguishable there;
`has_path?/2` tells them apart. Paths follow `A2UICore.Pointer`:
empty segments are dropped (`"//a///b//"` is `"/a/b"`), `~1` and `~0`
unescape in that order, and `__proto__`, `constructor` and `prototype`
are rejected on read and write with a `:data` error. List elements
address by RFC 6901 numeric segments (`"01"` is not an index).

## Writes

`set/3` writes a value at a path, creating missing containers on the
way: a list when the next segment is numeric, an object otherwise.
Lists are padded with `nil` up to the written index (at most
10000). Writing `nil` deletes: an object key is removed, a list
slot becomes `nil` (the list keeps its length), and deleting a path
that does not exist is a no-op. Writing `nil` at the root resets the
model to `%{}`. A primitive root, or a primitive in the middle of a
path, cannot be written through.

Writes are **atomic**: on error the original model is untouched. (The
upstream Python SDK leaves partial writes behind.)

## Subscriptions and notifications

`subscribe/2` registers interest in a path and returns an id plus the
current value. A write notifies every subscription whose path equals,
contains, or lies under the written path — compared segment by
segment, so `/foo` is not under `/foobar` — and only when the value
at the subscribed path actually changed. A notification is
`{subscription_id, normalized_path, new_value}`, ordered by
subscription id.

    iex> model = A2UICore.State.DataModel.new(%{"user" => %{"name" => "Ada"}})
    iex> {:ok, model, id, "Ada"} = A2UICore.State.DataModel.subscribe(model, "/user/name")
    iex> {:ok, model, [{^id, "/user/name", "Grace"}]} =
    ...>   A2UICore.State.DataModel.set(model, "/user/name", "Grace")
    iex> A2UICore.State.DataModel.get(model, "/user")
    {:ok, %{"name" => "Grace"}}

# `notification`
*since 0.1.0* 

```elixir
@type notification() :: {subscription_id(), String.t(), term()}
```

A change notification: subscription, its normalized path, the new value.

# `subscription_id`
*since 0.1.0* 

```elixir
@type subscription_id() :: pos_integer()
```

Identifies one subscription for `unsubscribe/2` and notifications.

# `t`
*since 0.1.0* 

```elixir
@type t() :: %A2UICore.State.DataModel{
  data: term(),
  next_id: pos_integer(),
  subscriptions: [%{id: pos_integer(), path: String.t(), tokens: [String.t()]}]
}
```

A data model.

# `data`
*since 0.1.0* 

```elixir
@spec data(t()) :: term()
```

The whole value held by the model (what `get(model, "/")` returns).

# `delete`
*since 0.1.0* 

```elixir
@spec delete(t(), term()) ::
  {:ok, t(), [notification()]} | {:error, A2UICore.Error.t()}
```

Deletes the value at `path`; shorthand for `set(model, path, nil)`.

# `dispose`
*since 0.1.0* 

```elixir
@spec dispose(t()) :: t()
```

Detaches every subscription. The data is kept.

# `get`
*since 0.1.0* 

```elixir
@spec get(t(), term()) :: {:ok, term()} | {:error, A2UICore.Error.t()}
```

Reads the value at `path`; `{:ok, nil}` when nothing is there.

Returns `{:error, error}` (category `:data`) for a path that is not a
string or contains a forbidden segment.

# `get!`
*since 0.1.0* 

```elixir
@spec get!(t(), term()) :: term()
```

Like `get/2`, raising `A2UICore.Error` on a bad path.

# `has_path?`
*since 0.1.0* 

```elixir
@spec has_path?(t(), term()) :: {:ok, boolean()} | {:error, A2UICore.Error.t()}
```

Whether `path` physically exists — distinguishes an explicit `null`
from a missing key. The root always exists.

# `new`
*since 0.1.0* 

```elixir
@spec new(term()) :: t()
```

Creates a model holding `initial` (`nil` means an empty object).

# `set`
*since 0.1.0* 

```elixir
@spec set(t(), term(), term()) ::
  {:ok, t(), [notification()]} | {:error, A2UICore.Error.t()}
```

Writes `value` at `path` (`nil` deletes). Returns the new model and
the notifications the write caused, or an error leaving the model
untouched.

# `subscribe`
*since 0.1.0* 

```elixir
@spec subscribe(t(), term()) ::
  {:ok, t(), subscription_id(), term()} | {:error, A2UICore.Error.t()}
```

Subscribes to `path`. Returns the new model, the subscription id and
the value currently at the path.

# `subscriptions`
*since 0.1.0* 

```elixir
@spec subscriptions(t()) :: [{subscription_id(), String.t()}]
```

The subscribed paths, as `{id, normalized_path}` pairs.

# `unsubscribe`
*since 0.1.0* 

```elixir
@spec unsubscribe(t(), subscription_id()) :: t()
```

Removes a subscription (unknown ids are ignored).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
