Current section
Files
Jump to
Current section
Files
lib/mead/session.ex
defmodule Mead.Session do
@moduledoc """
Per-scope render state over a shared `Mead.FontSet`.
A session owns the warm text-shaping state — a pool of parley contexts
reused across renders (and, next, the content-keyed shape cache).
Sessions are cheap to create; the expensive font parsing lives in the
`Mead.FontSet`, which any number of sessions share by reference.
set = Mead.FontSet.new(%{"Inter" => [%{path: "fonts/Inter-Regular.ttf"}]})
session = Mead.Session.new(set)
Mead.render(doc, session: session)
Recommended scoping: one `FontSet` per runtime; one session per
template (low volume) or per template-and-customer (high volume, keeps
each customer's repeated content warm and isolates cache memory). Hold
per-customer sessions as ordinary values (GenServer state, ETS,
Registry) — they are plain resource references and are reclaimed
normally when dropped.
With no font options at all, `Mead` renders through a lazily-built
global default session (fonts from the `:mead_pdf, :fonts` application
env) whose handle lives in `:persistent_term` — the default path parses
fonts once per VM, not once per render.
"""
alias Mead.FontSet
defstruct [:ref, :font_set]
@opaque t :: %__MODULE__{ref: reference(), font_set: FontSet.t()}
@default_key {Mead, :default_session}
@default_shape_cache_entries 4096
@doc """
Creates a session over a parsed `Mead.FontSet`.
## Options
* `:shape_cache_entries` - capacity (in paragraphs) of the session's
content-keyed shape cache, default #{@default_shape_cache_entries}.
Repeated text — the same label across renders, repeated cell
values, cloned table headers — is shaped once and replayed from the
cache. Bounded LRU, evicted inline, dropped with the session.
`0` disables the cache.
"""
@spec new(FontSet.t(), keyword()) :: t()
def new(%FontSet{} = set, opts \\ []) do
entries = Keyword.get(opts, :shape_cache_entries, @default_shape_cache_entries)
if !(is_integer(entries) and entries >= 0) do
raise ArgumentError,
":shape_cache_entries must be a non-negative integer, got: #{inspect(entries)}"
end
%__MODULE__{ref: Mead.Native.session_new(set.ref, entries), font_set: set}
end
@doc """
The global default session, built on first use from the `:mead_pdf,
:fonts` application env (same shape as `Mead.FontSet.new/2`, with the
`:mead_pdf, :font_fallback` env as the fallback chain) and kept in
`:persistent_term`.
Only this set-once handle uses `persistent_term`; per-customer sessions
should be held as plain values (updating `persistent_term` triggers a
global GC scan, which is fine at boot and wrong for churn).
"""
@spec default() :: t()
def default do
case :persistent_term.get(@default_key, nil) do
nil ->
set =
FontSet.new(
Application.get_env(:mead_pdf, :fonts, %{}),
fallback: Application.get_env(:mead_pdf, :font_fallback, []),
pool: Application.get_env(:mead_pdf, :font_pool, [])
)
session = new(set)
# Concurrent first calls may race and both build; last write wins
# and every built session is valid — acceptable for a boot path.
:persistent_term.put(@default_key, session)
session
session ->
session
end
end
@doc """
Drops the global default session; the next default render rebuilds it.
Call after changing the `:mead_pdf, :fonts` application env at runtime.
"""
@spec reset_default() :: :ok
def reset_default do
:persistent_term.erase(@default_key)
:ok
end
end