MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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

# 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

Summary

Functions

Returns a specification to start this module under a supervisor.

Clears all data from all tables.

Returns the number of pages in the registry.

Returns the number of pages for a specific site.

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

Deletes a page by ID and removes from all indexes.

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

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

Fast check if a page exists.

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

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

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

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

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

Fast check if a path exists for a site.

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

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

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

Saves page data to the ETS tables with all fields.

Batch save multiple pages for better performance.

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

Returns detailed statistics about the registry.

Stream paginated results for memory-efficient processing.

Functions

child_spec(init_arg)

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(site)

Returns the number of pages for a specific site.

count_by_site_and_section(site, section)

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

delete(id)

Deletes a page by ID and removes from all indexes.

delete_by_site(site)

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

dynamic_path?(path)

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

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

exists?(id)

Fast check if a page exists.

find_by_dynamic_path(path, site)

@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(path, 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!(path, site, section \\ "Page")

get(id)

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

get!(id)

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

paginate(opts)

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?(path, site)

Fast check if a path exists for a site.

rebuild_indexes()

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

resolve(path, site)

@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!(path, site, section \\ "Page")

@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(attrs)

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(pages)

Batch save multiple pages for better performance.

start_link(opts \\ [])

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

stats()

Returns detailed statistics about the registry.

stream_paginated(opts)

Stream paginated results for memory-efficient processing.