berrypod/lib/berrypod/pages/page_cache.ex
jamey cf627bd585 add custom page data model with split changesets and CRUD context
Stage 1 of custom CMS pages. Adds type/published/meta/nav fields to
pages schema, splits changeset into system vs custom (with slug format
validation and reserved path exclusion), adds create/update/delete
functions with auto-redirect on slug change, and warms custom pages
in ETS cache. 62 pages tests, 1426 total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:56:19 +00:00

100 lines
1.9 KiB
Elixir

defmodule Berrypod.Pages.PageCache do
@moduledoc """
GenServer that maintains an ETS table for caching page definitions.
Same pattern as `Berrypod.Theme.CSSCache`. Pages are cached by slug
for O(1) lookups. Invalidated on save, warmed on startup.
"""
use GenServer
@table_name :page_cache
## Client API
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Gets a cached page by slug. Returns `{:ok, page_data}` or `:miss`."
def get(slug) do
case :ets.lookup(@table_name, slug) do
[{^slug, page_data}] -> {:ok, page_data}
[] -> :miss
end
end
@doc "Caches a page definition by slug."
def put(slug, page_data) do
:ets.insert(@table_name, {slug, page_data})
:ok
end
@doc "Invalidates a single cached page."
def invalidate(slug) do
:ets.delete(@table_name, slug)
:ok
rescue
ArgumentError -> :ok
end
@doc "Invalidates all cached pages."
def invalidate_all do
:ets.delete_all_objects(@table_name)
:ok
rescue
ArgumentError -> :ok
end
@doc "Warms the cache by loading all pages from the DB."
def warm do
alias Berrypod.Pages
alias Berrypod.Pages.Page
for slug <- Page.system_slugs() do
page = Pages.get_page_from_db(slug)
put(slug, page)
end
import Ecto.Query
custom_slugs =
Berrypod.Repo.all(from p in Page, where: p.type == "custom", select: p.slug)
for slug <- custom_slugs do
case Pages.get_page_from_db(slug) do
nil -> :ok
page -> put(slug, page)
end
end
:ok
end
## Server callbacks
@impl true
def init(_opts) do
:ets.new(@table_name, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: false
])
{:ok, %{}, {:continue, :warm}}
end
@impl true
def handle_continue(:warm, state) do
try do
warm()
rescue
_ -> :ok
end
{:noreply, state}
end
end