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

High-performance ETS-based state manager for multi-site configuration and lookup.

Manages runtime site data with optimized storage and retrieval using multiple ETS
indexes for different access patterns. Designed for multi-tenant applications
requiring fast site resolution, priority-based ordering, and efficient
site management operations.

## Architecture

Uses a multi-table ETS architecture with specialized indexes:
- **Main Table**: Primary storage with complete site data
- **Host Index**: O(1) lookups by hostname for request routing
- **Name Index**: O(1) lookups by site name for administration
- **Origins Index**: O(1) lookup of whether a site allows a CORS origin — see `allows_origin?/2`
- **Page Editions Index**: the live editions of each page group, so a page names its other
  languages without a query — see `page_editions/2`
- **Conflict Detection**: Automatic hostname and name uniqueness validation

## Data Structure

Each site record contains:
- `id`: Unique site identifier (UUID or database ID)
- `name`: Human-readable site name for administration
- `host`: Hostname for HTTP request routing
- `priority`: Site priority for ordering (higher = more important)
- `active`: Boolean flag for site availability
- `master`: Boolean flag indicating if this is the master site

## Key Features

- **O(1) Host Resolution**: Instant site lookup by hostname for routing
- **Conflict Prevention**: Automatic hostname and name uniqueness validation
- **Priority Ordering**: Site ranking and display ordering
- **Active Status Management**: Site availability control
- **Batch Operations**: Efficient bulk site loading
- **Flexible Pagination**: Priority and name-based sorting options
- **Memory Streaming**: Process large site lists without memory overhead
- **Concurrent Access**: Read/write concurrency optimization

## Performance Characteristics

- Host/name lookups: O(1)
- Pagination queries: O(n log n) where n = total sites
- Memory usage: ~150-200 bytes per site record
- Concurrent reads: Fully optimized
- Batch inserts: ~8x faster than individual saves

## Usage Examples

```elixir
# Register a new site
SiteState.save(%{
  id: "site_123",
  name: "My Blog",
  host: "myblog.com",
  priority: 90,
  active: true,
  master: false
})

# Fast host-based site resolution
{:ok, site} = SiteState.get_by_host("myblog.com")

# Administrative site lookup
{:ok, site} = SiteState.get_by_name("My Blog")

# Priority-ordered site listing
%{data: sites, total: count} = SiteState.paginate(%{
  page: 1,
  limit: 20,
  active: true,
  sort: :priority,
  order: :desc
})

# Batch site loading for performance
SiteState.save_batch([
  %{id: "site1", name: "Site One", host: "one.com", priority: 100, active: true, master: true},
  %{id: "site2", name: "Site Two", host: "two.com", priority: 80, active: true, master: false}
])

# Memory-efficient site processing
SiteState.stream(%{active: true})
|> Stream.each(&process_site/1)
|> Stream.run()
```

## Request Routing Integration

```elixir
# In a Phoenix plug or controller
def resolve_site(conn, _opts) do
  host = get_req_header(conn, "host") |> List.first()
  
  case SiteState.get_by_host(host) do
    {:ok, site} when site.active ->
      assign(conn, :current_site, site)
    _ ->
      send_resp(conn, 404, "Site not found") |> halt()
  end
end
```

## Integration Patterns

- **Multi-tenant Routing**: Host-based tenant resolution
- **Site Administration**: Management interface with priority ordering
- **Load Balancing**: Priority-based request distribution
- **Feature Flags**: Active/inactive site management
- **Analytics**: Site performance and usage tracking
- **CDN Configuration**: Host-based asset routing

## Production Considerations

- Monitor site lookup performance and memory usage
- Implement proper error handling for inactive sites
- Use batch operations for initial site loading
- Consider hostname validation at application boundaries
- Implement audit logging for site configuration changes
- Use streaming for large-scale site operations

# `allows_origin?`

```elixir
@spec allows_origin?(term(), MishkaCmsCore.Runtime.OriginValidator.origin()) ::
  boolean()
```

Whether the site `site_id` allows `origin` for CORS, an origin as
`MishkaCmsCore.Runtime.OriginValidator.request_origin/1` reads a request's `Origin` header. One
ETS lookup: the origins a site allows are worked out by `OriginValidator.site_origins/1` when the
site is saved, and dropped with it.

    {:ok, origin} = OriginValidator.request_origin("https://app.example.com")
    SiteState.allows_origin?(site.id, origin)  #=> true

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

Clears all data from all tables.

# `component?`

```elixir
@spec component?(String.t() | nil, String.t()) :: boolean()
```

Whether the site `site_id` — `nil` for the global scope — has an active component named `name`.
One ETS lookup, and nothing is made from a name it does not know.

# `count`

Returns the number of sites.

# `delete`

Deletes a site by ID.

# `delete_component`

```elixir
@spec delete_component(String.t() | nil, String.t()) :: :ok
```

Forgets the component named `name` of the site `site_id` — `nil` for the global scope.

# `delete_page_edition`

```elixir
@spec delete_page_edition(map()) :: :ok
```

Takes the page row `page` out of the editions index — archived, deleted or switched off.

# `exists?`

Fast check if a site exists.

# `get`

Gets site by ID.

# `get!`

Gets site by ID, returns nil if not found.

# `get_active_sites`

Gets all active sites sorted by priority DESC.
Note: For large datasets, consider using paginate/1 or stream/1 instead.

# `get_by_host`

High-performance lookup by host (O(1)).

# `get_by_host!`

High-performance lookup by host, returns nil if not found.

# `get_by_master`

Gets the master site.
Returns {:ok, site} if master site exists, {:error, :not_found} otherwise.

# `get_by_master!`

Gets the master site, returns nil if not found.

# `get_by_name`

High-performance lookup by name (O(1)).

# `get_by_priority_range`

Gets sites by priority range.

# `host_exists?`

Fast check if a host exists.

# `page_editions`

```elixir
@spec page_editions(String.t(), String.t()) :: [
  %{path: String.t(), language: String.t() | nil, extra: map() | nil}
]
```

The active, unarchived editions of the language group `group` on the site `site_id`, each as its
stored path, its language and its `extra` — what `MishkaCmsCore.Runtime.PageEditions.editions/2`
offers beside a page. One ETS range read, and no query: a group nothing is known of has no
editions.

    SiteState.page_editions(site.id, about.language_group_id)
    #=> [%{path: "/about", language: "en", extra: %{}},
    #    %{path: "/fa/about", language: "fa", extra: %{}}]

# `paginate`

High-performance paginated query with sorting by priority DESC.

## Options
  * `:page` - Page number (default: 1)
  * `:limit` - Items per page (default: 20)
  * `:active` - Filter by active status (default: nil, shows all)
  * `:sort` - Sort field, :priority or :name (default: :priority)
  * `:order` - Sort order, :asc or :desc (default: :desc for priority, :asc for name)

## Examples

    MishkaCmsCore.Runtime.States.SiteState.paginate(%{page: 1, limit: 20, active: true})

# `put_component`

```elixir
@spec put_component(String.t() | nil, String.t()) :: :ok
```

Remembers that the site `site_id` — `nil` for the global scope — has an active component named
`name`.

# `put_page_edition`

```elixir
@spec put_page_edition(map()) :: :ok
```

Keeps the page row `page` in the editions index as it is now: under its site and language group
while it is active and unarchived, and out of it otherwise. A page moved to another group leaves
its old one in the same call, and no other row is touched.

`MishkaCmsCore.Runtime.Resources.Notifiers.PageEditions` calls it on the node that wrote the row,
and this process on every other one, from the row the write's broadcast carries.

# `restore`

```elixir
@spec restore(%{required(atom()) =&gt; [tuple()]}) :: :ok
```

Puts the tables back to a `snapshot/0` (test helper). It reads no database, so it works from an
`on_exit`, once the test's rows have rolled back and only these entries are left of them.

# `save`

Saves site data to the ETS tables.
Returns :ok or {:error, :host_already_exists} / {:error, :name_already_exists}

# `save_batch`

Batch save multiple sites for better performance during boot.
Skips conflict checking for speed - use only during initial load!

# `save_page_editions`

```elixir
@spec save_page_editions([map()]) :: :ok
```

Puts every page of `pages` in the editions index at once — the boot's batch, from a read of the
active pages alone (see `MishkaCmsCore.Runtime.Handlers.BootHandler.load_page_editions/0`).

# `set_master`

Sets a site as master, unsetting any existing master site.

# `snapshot`

```elixir
@spec snapshot() :: %{required(atom()) =&gt; [tuple()]}
```

Everything the tables hold now, for `restore/1` (test helper).

    snapshot = SiteState.snapshot()
    on_exit(fn -> SiteState.restore(snapshot) end)

# `start_link`

Starts the SiteState GenServer and creates the ETS tables.

# `stream`

Stream all sites for memory-efficient processing.

## Options
  * `:active` - Filter by active status (default: nil, shows all)
  * `:sort` - Sort field, :priority or :name (default: :priority)
  * `:order` - Sort order, :asc or :desc (default: :desc for priority, :asc for name)
  * `:chunk_size` - Number of records per chunk (default: 100)

## Examples

    MishkaCmsCore.Runtime.States.SiteState.stream(%{active: true})
    |> Stream.each(fn site -> IO.inspect(site) end)
    |> Stream.run()

# `unset_master`

Unsets the master site (finds the current master and sets its master flag to false).
Returns {:ok, site} with the updated site, or {:error, :not_found} if no master exists.

---

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