Packages

phoenix_kit

1.7.19
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules blogging pubsub.ex
Raw

lib/modules/blogging/pubsub.ex

defmodule PhoenixKit.Modules.Blogging.PubSub do
@moduledoc """
PubSub integration for real-time blogging updates.
Provides broadcasting and subscription for blog post changes,
enabling live updates across all connected admin clients.
## Features
- Post lifecycle events (create, update, delete, status change)
- Collaborative editing with real-time form state sync
- Owner/spectator model for concurrent editing
"""
alias PhoenixKit.PubSub.Manager
@topic_prefix "blogging"
@topic_editor_forms "blogging:editor_forms"
@topic_blogs "blogging:blogs"
# ============================================================================
# Blog-Level Updates (blog creation/deletion)
# ============================================================================
@doc """
Returns the topic for global blog updates (create, delete).
"""
def blogs_topic, do: @topic_blogs
@doc """
Subscribes the current process to blog updates (creation/deletion).
"""
def subscribe_to_blogs do
Manager.subscribe(blogs_topic())
end
@doc """
Unsubscribes the current process from blog updates.
"""
def unsubscribe_from_blogs do
Manager.unsubscribe(blogs_topic())
end
@doc """
Broadcasts a blog created event.
"""
def broadcast_blog_created(blog) do
Manager.broadcast(blogs_topic(), {:blog_created, blog})
end
@doc """
Broadcasts a blog deleted event.
"""
def broadcast_blog_deleted(blog_slug) do
Manager.broadcast(blogs_topic(), {:blog_deleted, blog_slug})
end
@doc """
Broadcasts a blog updated event.
"""
def broadcast_blog_updated(blog) do
Manager.broadcast(blogs_topic(), {:blog_updated, blog})
end
# ============================================================================
# Post List Updates (simple refresh)
# ============================================================================
@doc """
Returns the topic for a specific blog's posts.
"""
def posts_topic(blog_slug) do
"#{@topic_prefix}:#{blog_slug}:posts"
end
@doc """
Subscribes the current process to post updates for a blog.
"""
def subscribe_to_posts(blog_slug) do
Manager.subscribe(posts_topic(blog_slug))
end
@doc """
Unsubscribes the current process from post updates for a blog.
"""
def unsubscribe_from_posts(blog_slug) do
Manager.unsubscribe(posts_topic(blog_slug))
end
@doc """
Broadcasts a post created event.
"""
def broadcast_post_created(blog_slug, post) do
Manager.broadcast(posts_topic(blog_slug), {:post_created, post})
end
@doc """
Broadcasts a post updated event.
"""
def broadcast_post_updated(blog_slug, post) do
Manager.broadcast(posts_topic(blog_slug), {:post_updated, post})
end
@doc """
Broadcasts a post deleted event.
"""
def broadcast_post_deleted(blog_slug, post_path) do
Manager.broadcast(posts_topic(blog_slug), {:post_deleted, post_path})
end
@doc """
Broadcasts a post status changed event.
"""
def broadcast_post_status_changed(blog_slug, post) do
Manager.broadcast(posts_topic(blog_slug), {:post_status_changed, post})
end
@doc """
Broadcasts that a new version was created for a post.
"""
def broadcast_version_created(blog_slug, post) do
Manager.broadcast(posts_topic(blog_slug), {:version_created, post})
end
@doc """
Broadcasts that the live version changed for a post.
"""
def broadcast_version_live_changed(blog_slug, post_slug, version) do
Manager.broadcast(posts_topic(blog_slug), {:version_live_changed, post_slug, version})
end
@doc """
Broadcasts that a version was deleted from a post.
"""
def broadcast_version_deleted(blog_slug, post_slug, version) do
Manager.broadcast(posts_topic(blog_slug), {:version_deleted, post_slug, version})
end
# ============================================================================
# Post-Level Updates (translation changes)
# ============================================================================
@doc """
Returns the topic for a specific post's translation updates.
This allows all editors of different language versions to receive updates
when new translations are added.
"""
def post_translations_topic(blog_slug, post_slug) do
"#{@topic_prefix}:#{blog_slug}:post:#{post_slug}:translations"
end
@doc """
Subscribes to translation updates for a specific post.
"""
def subscribe_to_post_translations(blog_slug, post_slug) do
Manager.subscribe(post_translations_topic(blog_slug, post_slug))
end
@doc """
Unsubscribes from translation updates for a specific post.
"""
def unsubscribe_from_post_translations(blog_slug, post_slug) do
Manager.unsubscribe(post_translations_topic(blog_slug, post_slug))
end
@doc """
Broadcasts that a new translation was created for a post.
"""
def broadcast_translation_created(blog_slug, post_slug, language) do
Manager.broadcast(
post_translations_topic(blog_slug, post_slug),
{:translation_created, blog_slug, post_slug, language}
)
end
@doc """
Broadcasts that a translation was deleted from a post.
"""
def broadcast_translation_deleted(blog_slug, post_slug, language) do
Manager.broadcast(
post_translations_topic(blog_slug, post_slug),
{:translation_deleted, blog_slug, post_slug, language}
)
end
# ============================================================================
# Editor Save Sync (last-save-wins model)
# ============================================================================
@doc """
Broadcasts that a post was saved, so other editors can reload from disk.
The `source` is the socket.id of the saver, so they don't reload their own save.
"""
def broadcast_editor_saved(form_key, source) do
Manager.broadcast(
editor_form_topic(form_key),
{:editor_saved, form_key, source}
)
end
# ============================================================================
# Collaborative Editor (real-time form sync)
# ============================================================================
@doc """
Returns the topic for a specific editor form.
The form_key uniquely identifies a post being edited:
- For existing posts: "blog_slug:post_path" or "blog_slug:slug"
- For new posts: "blog_slug:new:language"
"""
def editor_form_topic(form_key) do
"#{@topic_editor_forms}:#{form_key}"
end
@doc """
Returns the presence topic for tracking editors of a post.
"""
def editor_presence_topic(form_key) do
"blogging:presence:editor:#{form_key}"
end
@doc """
Subscribes to collaborative events for a specific editor form.
"""
def subscribe_to_editor_form(form_key) do
Manager.subscribe(editor_form_topic(form_key))
end
@doc """
Unsubscribes from collaborative events for a specific editor form.
"""
def unsubscribe_from_editor_form(form_key) do
Manager.unsubscribe(editor_form_topic(form_key))
end
@doc """
Broadcasts a form state change to all subscribers.
Options:
- `:source` - The source identifier to prevent self-echoing
"""
def broadcast_editor_form_change(form_key, payload, opts \\ []) do
Manager.broadcast(
editor_form_topic(form_key),
{:editor_form_change, form_key, payload, Keyword.get(opts, :source)}
)
end
@doc """
Broadcasts a sync request for new joiners to get current state.
"""
def broadcast_editor_sync_request(form_key, requester_socket_id) do
Manager.broadcast(
editor_form_topic(form_key),
{:editor_sync_request, form_key, requester_socket_id}
)
end
@doc """
Broadcasts a sync response with current form state.
"""
def broadcast_editor_sync_response(form_key, requester_socket_id, state) do
Manager.broadcast(
editor_form_topic(form_key),
{:editor_sync_response, form_key, requester_socket_id, state}
)
end
# ============================================================================
# Cache Updates (for live admin UI updates)
# ============================================================================
@doc """
Returns the topic for cache updates for a specific blog.
"""
def cache_topic(blog_slug) do
"#{@topic_prefix}:#{blog_slug}:cache"
end
@doc """
Subscribes the current process to cache updates for a blog.
"""
def subscribe_to_cache(blog_slug) do
Manager.subscribe(cache_topic(blog_slug))
end
@doc """
Unsubscribes the current process from cache updates for a blog.
"""
def unsubscribe_from_cache(blog_slug) do
Manager.unsubscribe(cache_topic(blog_slug))
end
@doc """
Broadcasts that the cache state has changed (file regenerated, memory loaded, etc).
"""
def broadcast_cache_changed(blog_slug) do
Manager.broadcast(cache_topic(blog_slug), {:cache_changed, blog_slug})
end
# ============================================================================
# Form Key Helpers
# ============================================================================
@doc """
Generates a form key for a post being edited.
The form key includes the language to allow concurrent editing of different
translations of the same post.
## Examples
generate_form_key("blog", %{path: "blog/my-post/v1/en.phk"})
# => "blog:blog/my-post/v1/en.phk"
generate_form_key("blog", %{slug: "my-post", language: "en"})
# => "blog:my-post:en"
generate_form_key("blog", %{slug: "my-post", language: "en"}, :new)
# => "blog:new:en"
"""
def generate_form_key(blog_slug, post, mode \\ :edit)
# Path already includes language (e.g., "blog/my-post/v1/en.phk")
def generate_form_key(blog_slug, %{path: path}, :edit) when is_binary(path) do
"#{blog_slug}:#{path}"
end
# Slug mode - include language for per-language locking
def generate_form_key(blog_slug, %{slug: slug, language: lang}, :edit)
when is_binary(slug) and is_binary(lang) do
"#{blog_slug}:#{slug}:#{lang}"
end
# Fallback for slug without language (shouldn't happen in practice)
def generate_form_key(blog_slug, %{slug: slug}, :edit) when is_binary(slug) do
"#{blog_slug}:#{slug}"
end
def generate_form_key(blog_slug, %{language: lang}, :new) do
"#{blog_slug}:new:#{lang}"
end
def generate_form_key(blog_slug, _post, :new) do
"#{blog_slug}:new"
end
def generate_form_key(blog_slug, _, _) do
"#{blog_slug}:unknown"
end
end