# `MishkaCmsCore.Runtime.States.EditorSessionState`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/states/editor_session_state.ex#L5)

The ETS table behind the page builder's live editing sessions, keyed by session token.

A session holds the working tree, the selection, the undo history and the clipboard — everything
the editor needs between opening a page and saving it. Keeping it here rather than in a LiveView's
assigns is what lets the **two** LiveViews of the editor (the chrome and the canvas inside its
iframe) look at one document: they hold a token, not a copy.

Crash-safe: a GenServer owns a `:named_table, :public` table and every read guards on
`:ets.whereis/1`, so a caller that races the table's creation gets `:error` instead of an
ArgumentError.

## Every change is a compare-and-swap

One session has several writers at once — the chrome, the canvas, and the tasks each of them
starts (the history flush, the autosave, the palette load). Each of them reads the row, works out
the next session, and writes it back, and a plain `:ets.insert/2` at the end of that let the last
one to finish put back everything it had read: an edit announced to every screen and then silently
undone by a flush that had read the session a moment before it. `update/2` stamps each row with a
version and writes only over the version it read, so a change that lost the race is worked out
again on the session as the winner left it.

Sessions are swept hourly: anything untouched for 24 hours is dropped. Nothing is lost by that,
because a live session autosaves to a draft, and a draft is a real row.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `count`

```elixir
@spec count() :: non_neg_integer()
```

How many sessions are open right now. Used by the admin runtime-health panel and by tests.

# `delete`

```elixir
@spec delete(String.t()) :: :ok
```

Drops a session — what closing the editor does.

# `find`

```elixir
@spec find(String.t(), String.t(), String.t() | nil, integer() | nil) ::
  {:ok, struct()} | :error
```

The session this person already has open on this document, if any — the newest one.

Reopening the editor used to mint a fresh token and rebuild from the database, so a reload, a
crashed canvas or a mis-click threw away everything since the last save. A session survives all
three; it was only the lookup that was missing.

Scoped to the actor as well as the document: two people editing the same page get their own
sessions and meet through `Presence`, which is what the locking and the collaborator list already
assume.

Matched over the same table rather than kept in a second index — an index would be one more thing
to invalidate on delete and on the hourly sweep, for a table that holds tens of rows.

# `get`

```elixir
@spec get(String.t()) :: {:ok, struct()} | :error
```

Fetches a session by token.

# `put`

```elixir
@spec put(String.t(), struct()) :: struct()
```

Stores a new session and stamps it as touched. Returns the session unchanged so it composes into a
pipeline; a write before the table exists is a no-op rather than a crash.

For a session being OPENED. Anything changing a session that is already stored goes through
`update/2`, because this writes whatever it is handed over whatever is there.

A WRITE IS NOT A READ. Answering a missing table forgivingly is right for a read — racing the
table's creation should not raise — but a write that goes nowhere and reports success is how an
editor comes to believe it saved something. In development that raises, because the table is either
up or the application is misconfigured and both are worth knowing about.

# `start_link`

# `tokens`

```elixir
@spec tokens() :: [String.t()]
```

Every open session token. Ordering is undefined.

# `update`

```elixir
@spec update(String.t(), (struct() -&gt; {:write, struct(), reply} | {:skip, reply})) ::
  {:ok, reply} | :error | {:error, :contended}
when reply: term()
```

Changes a stored session atomically.

`change` receives the session as stored and answers `{:write, session, reply}` to store `session`,
or `{:skip, reply}` to leave the row as it is. Either way the answer is `{:ok, reply}`; `:error`
when there is no such session.

The write lands only over the version `change` read. When another write landed in between, nothing
is stored and `change` runs again on the session that write left — so `change` must do nothing it
cannot repeat, and anything that must happen once belongs after this returns. Losing that race
more than a bounded number of times in a row is a violation in development and
`{:error, :contended}` otherwise.

---

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