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.
Summary
Functions
Returns a specification to start this module under a supervisor.
How many sessions are open right now. Used by the admin runtime-health panel and by tests.
Drops a session — what closing the editor does.
The session this person already has open on this document, if any — the newest one.
Fetches a session by token.
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.
Every open session token. Ordering is undefined.
Changes a stored session atomically.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec count() :: non_neg_integer()
How many sessions are open right now. Used by the admin runtime-health panel and by tests.
@spec delete(String.t()) :: :ok
Drops a session — what closing the editor does.
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.
Fetches a session by token.
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.
@spec tokens() :: [String.t()]
Every open session token. Ordering is undefined.
@spec update(String.t(), (struct() -> {: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.