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

High-performance ETS-based state manager for web page metadata with multi-index support.

Manages runtime metadata for web pages, posts, and content with optimized storage and
retrieval using multiple ETS tables for different access patterns. Designed for
high-concurrency web applications requiring fast page lookups, pagination,
and content organization.

## Architecture

Uses a multi-table ETS architecture with specialized indexes:
- **Main Table**: Primary storage with full page data
- **Path Index**: O(1) lookups by path and site combination
- **Site Index**: Efficient site-based filtering
- **Section Index**: Content organization by site and section
- **Sort Index**: Ordered set for priority-based pagination

## Data Structure

Each page record contains:
- `id`: Unique page identifier
- `site`: Site identifier for multi-tenant support
- `path`: URL path for routing
- `title`: Page title for display
- `created_at`: Timestamp for ordering
- `section`: Content category (Page, Blog, etc.)
- `priority`: Sort priority (higher = more important)

## Key Features

- **O(1) Path Lookups**: Instant page resolution by path/site
- **Optimized Pagination**: Priority-based sorting with efficient offset/limit
- **Memory Streaming**: Process large datasets without memory overhead
- **Conflict Prevention**: Automatic path uniqueness validation
- **Batch Operations**: High-performance bulk data operations
- **Multi-tenant Support**: Site-based data isolation
- **Concurrent Access**: Read/write concurrency optimization

## Performance Characteristics

- Path lookups: O(1)
- Pagination queries: O(n log n) where n = matching records
- Memory usage: ~200-300 bytes per page record
- Concurrent reads: Fully optimized
- Batch inserts: ~10x faster than individual saves

## Usage Examples

```elixir
# Save a page
WebRunTimeMetaDataState.save(%{
  id: "blog_post_123",
  site: "myblog",
  path: "/posts/hello-world",
  title: "Hello World",
  created_at: DateTime.utc_now(),
  section: "Blog",
  priority: 90
})

# Fast path lookup
{:ok, page} = WebRunTimeMetaDataState.find_by_path_and_site("/posts/hello-world", "myblog")

# Paginated results
%{data: pages, total: count} = WebRunTimeMetaDataState.paginate(%{
  site: "myblog",
  section: "Blog",
  page: 1,
  limit: 10
})

# Memory-efficient streaming
WebRunTimeMetaDataState.stream_paginated(%{site: "myblog"})
|> Stream.each(&process_page/1)
|> Stream.run()
```

## Integration Patterns

- **Router Integration**: Fast route resolution for dynamic pages
- **SEO Management**: Title and metadata organization
- **Content Management**: Section-based content organization
- **Analytics**: Priority-based content ranking
- **Multi-site CMSs**: Tenant-isolated page management

## Production Considerations

- Monitor memory usage with `stats/0` function
- Use batch operations for initial data loading
- Consider streaming for large dataset processing
- Implement cleanup strategies for unused pages
- Path conflicts are prevented automatically

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

Clears all data from all tables.

# `count`

Returns the number of pages in the registry.

# `count_by_site`

Returns the number of pages for a specific site.

# `count_by_site_and_section`

Returns the number of pages for a specific site and section.

# `delete`

Deletes a page by ID and removes from all indexes.

# `delete_by_site`

Deletes every routing entry for a site and returns the count removed.

# `dynamic_path?`

```elixir
@spec dynamic_path?(String.t()) :: boolean()
```

Whether a stored path is a pattern rather than a literal.

# `exists?`

Fast check if a page exists.

# `find_by_dynamic_path`

```elixir
@spec find_by_dynamic_path(String.t(), String.t()) ::
  {:ok, map(), map()} | {:error, :not_found}
```

The page whose stored path PATTERN matches this request, with the segments it captured.

A page stored at `/post/:category/:slug` answers `/post/elixir/hello-world` and captures
`%{"category" => "elixir", "slug" => "hello-world"}`. `*rest` captures everything left, joined,
which is how one page serves a whole subtree.

## Why this is ours and not Phoenix's

`Phoenix.Router` compiles its routes: the match is generated at BUILD time from macros, so it
cannot know a path an admin typed into the editor this morning. That is exactly why this app
routes `live "/*path"` and resolves the rest itself. There is no Phoenix or Plug function that
matches a runtime pattern STRING against runtime segments — `Plug.Router.Utils.build_path_match/1`
returns AST for the compiler — so the matcher below is the part that genuinely has to be ours.

`MishkaCmsCore.Runtime.Resources.Validations.PathFormat` has always allowed these paths, naming
"route params like `:id`, and wildcards like `*rest`". Until now nothing matched them.

## Precedence

An exact path always wins: `find_by_path_and_site/2` only reaches this after the literal ETS
lookup misses, so no URL that resolves today resolves differently.

Between two patterns that both match, the one with FEWER captures wins — `/post/new` beats
`/post/:slug`, and `/post/:slug` beats `/*rest`. Ties break on the page's own `priority`, which is
the field `Page`'s `:by_priority` read already exists to order by, and then on id so the answer is
stable rather than dependent on ETS order.

## A capture named `lang`

Matches only one of the site's languages, in whichever segment it sits — see
`MishkaCmsCore.Runtime.Languages`. `/:lang/about` does not answer `/xyz/about`: that pattern is
passed over as though it had not matched, so the request goes to the next pattern or to the 404.
The site's languages are read only when some pattern of the site captures `lang`.

# `find_by_path_and_site`

The record STORED at this path, for a caller naming one it already knows.

A layout is reached by the path the page names it with and an error page by the path its row
carries; neither is a URL a visitor typed. So this looks the path up as written — a pattern path
included — which is what separates it from `resolve/2`.

# `find_by_path_and_site!`

# `get`

Gets page data by ID using direct lookup (O(1)).

# `get!`

Gets page data by ID, returns nil if not found.

# `paginate`

High-performance paginated query with sorting by priority DESC, created_at ASC.

## Examples

    MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState.paginate(%{
      site: "example",
      section: "blog",
      page: 1,
      limit: 20
    })

# `path_exists?`

Fast check if a path exists for a site.

# `rebuild_indexes`

Rebuilds all indexes from the main table (useful for maintenance).

# `resolve`

```elixir
@spec resolve(String.t(), String.t()) :: {:ok, map(), map()} | {:error, :not_found}
```

The page for a REQUEST path, and whatever its pattern captured.

`%{}` for a literal page, which is almost all of them.

A REQUEST NEVER CARRIES PATTERN SYNTAX, and refusing it here is the whole of this clause. A
pattern page is stored under its pattern — `/:lang/blog` — and the path index is keyed by the
stored string, so typing that pattern into the address bar matched the index EXACTLY and was
served as though it were a literal page: `/:lang/blog` and `/:lang/blog/tags` answered 200, in
no language, from a row nobody could reach that way on purpose. The dynamic matcher was never
consulted, which is why `Languages.admits?/3` did not stop it — `/xyz/blog` was refused all
along, and only the pattern's own text got through.

Every capture is refused, not just `lang`: `/post/:slug` would otherwise answer with `:slug` as
the slug, and `*rest` the same. `dynamic_path?/1` is the same predicate that decides what earns a
dynamic row, so a path is a pattern here exactly when it is one there.

## An address that names no language

On a site whose primary language has no prefix (`MishkaCmsCore.Runtime.Languages.url_mode/1`), a
request whose first segment is not one of its languages is in the primary one
(`Languages.implied/2`), and is asked three questions in turn:

  1. the literal page stored at the path as typed — `/blog`;
  2. the literal page stored under the primary language — `/en/blog`;
  3. the best pattern for either form, by the precedence `find_by_dynamic_path/2` keeps, and the
     prefixed form when the two tie — `/:lang/blog` answers `/blog` as `/en/blog`.

A match through the prefixed form captures `lang` as the primary language, as `/en/blog` would
have, so a page's editions and the language of the response read it the same way. A site that
prefixes every language, and a request that names one, are asked about the path as typed alone.

# `resolve!`

```elixir
@spec resolve!(String.t(), String.t(), String.t()) :: {map(), map()}
```

`find_by_path_and_site!/3` with the captured segments beside the page.

Raises the same `NotFoundError`, so a caller that wants the captures does not have to handle a
different failure than one that does not.

# `save`

Saves page data to the ETS tables with all fields.

## Examples

    MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState.save(%{
      id: "page_123",
      site: "example",
      path: "/about",
      title: "About Us",
      created_at: ~U[2024-01-01 10:00:00Z],
      section: "Page",
      priority: 80
    })

# `save_batch`

Batch save multiple pages for better performance.

# `start_link`

Starts the MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState GenServer and creates the ETS tables.

# `stats`

Returns detailed statistics about the registry.

# `stream_paginated`

Stream paginated results for memory-efficient processing.

---

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