Packages
phoenix_kit
1.7.21
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
Current section
Files
lib/modules/publishing/pubsub.ex
defmodule PhoenixKit.Modules.Publishing.PubSub do
@moduledoc """
PubSub integration for real-time publishing updates.
Provides broadcasting and subscription for 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 "publishing"
@topic_editor_forms "publishing:editor_forms"
@topic_blogs "publishing:groups"
# ============================================================================
# Group-Level Updates (group creation/deletion)
# ============================================================================
@doc """
Returns the topic for global group updates (create, delete).
"""
def groups_topic, do: @topic_blogs
@doc """
Subscribes the current process to group updates (creation/deletion).
"""
def subscribe_to_groups do
Manager.subscribe(groups_topic())
end
@doc """
Unsubscribes the current process from group updates.
"""
def unsubscribe_from_groups do
Manager.unsubscribe(groups_topic())
end
@doc """
Broadcasts a group created event.
"""
def broadcast_group_created(group) do
Manager.broadcast(groups_topic(), {:group_created, group})
end
@doc """
Broadcasts a group deleted event.
"""
def broadcast_group_deleted(group_slug) do
Manager.broadcast(groups_topic(), {:group_deleted, group_slug})
end
@doc """
Broadcasts a group updated event.
"""
def broadcast_group_updated(group) do
Manager.broadcast(groups_topic(), {:group_updated, group})
end
# Backward compatibility aliases
@doc false
@deprecated "Use groups_topic/0 instead"
def blogs_topic, do: groups_topic()
@doc false
@deprecated "Use subscribe_to_groups/0 instead"
def subscribe_to_blogs, do: subscribe_to_groups()
@doc false
@deprecated "Use unsubscribe_from_groups/0 instead"
def unsubscribe_from_blogs, do: unsubscribe_from_groups()
@doc false
@deprecated "Use broadcast_group_created/1 instead"
def broadcast_blog_created(group), do: broadcast_group_created(group)
@doc false
@deprecated "Use broadcast_group_deleted/1 instead"
def broadcast_blog_deleted(group_slug), do: broadcast_group_deleted(group_slug)
@doc false
@deprecated "Use broadcast_group_updated/1 instead"
def broadcast_blog_updated(group), do: broadcast_group_updated(group)
# ============================================================================
# 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 (version and translation changes)
# ============================================================================
@doc """
Returns the topic for a specific post's version updates.
This allows editors to receive notifications when versions are created/deleted.
"""
def post_versions_topic(blog_slug, post_slug) do
"#{@topic_prefix}:#{blog_slug}:post:#{post_slug}:versions"
end
@doc """
Subscribes to version updates for a specific post.
"""
def subscribe_to_post_versions(blog_slug, post_slug) do
Manager.subscribe(post_versions_topic(blog_slug, post_slug))
end
@doc """
Unsubscribes from version updates for a specific post.
"""
def unsubscribe_from_post_versions(blog_slug, post_slug) do
Manager.unsubscribe(post_versions_topic(blog_slug, post_slug))
end
@doc """
Broadcasts that a new version was created for a post (to post-level topic).
"""
def broadcast_post_version_created(blog_slug, post_slug, version_info) do
Manager.broadcast(
post_versions_topic(blog_slug, post_slug),
{:post_version_created, blog_slug, post_slug, version_info}
)
end
@doc """
Broadcasts that a version was deleted from a post (to post-level topic).
"""
def broadcast_post_version_deleted(blog_slug, post_slug, version) do
Manager.broadcast(
post_versions_topic(blog_slug, post_slug),
{:post_version_deleted, blog_slug, post_slug, version}
)
end
@doc """
Broadcasts that the live/published version changed (to post-level topic).
"""
def broadcast_post_version_published(blog_slug, post_slug, version) do
Manager.broadcast(
post_versions_topic(blog_slug, post_slug),
{:post_version_published, blog_slug, post_slug, version}
)
end
@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
"publishing: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
@doc """
Broadcasts detailed cache operation info.
"""
def broadcast_cache_operation(blog_slug, operation, metadata \\ %{}) do
Manager.broadcast(cache_topic(blog_slug), {:cache_operation, blog_slug, operation, metadata})
end
# ============================================================================
# AI Translation Progress
# ============================================================================
@doc """
Broadcasts that AI translation has started.
Sent to posts_topic for blog listing UI indicator.
"""
def broadcast_translation_started(blog_slug, post_slug, target_languages) do
Manager.broadcast(
posts_topic(blog_slug),
{:translation_started, post_slug, length(target_languages)}
)
end
@doc """
Broadcasts that AI translation has completed (success or partial failure).
Sent to posts_topic to clear blog listing UI indicator.
"""
def broadcast_translation_completed(blog_slug, post_slug, results) do
Manager.broadcast(
posts_topic(blog_slug),
{:translation_completed, post_slug, results}
)
end
# ============================================================================
# Editor Presence for Blog Listing
# ============================================================================
@doc """
Returns the global topic for editor activity across a blog.
Used by blog listing to show who's editing what.
"""
def blog_editors_topic(blog_slug) do
"#{@topic_prefix}:#{blog_slug}:editors"
end
@doc """
Subscribes to editor activity for a blog (used by blog listing).
"""
def subscribe_to_blog_editors(blog_slug) do
Manager.subscribe(blog_editors_topic(blog_slug))
end
@doc """
Unsubscribes from editor activity for a blog.
"""
def unsubscribe_from_blog_editors(blog_slug) do
Manager.unsubscribe(blog_editors_topic(blog_slug))
end
@doc """
Broadcasts that a user started editing a post.
"""
def broadcast_editor_joined(blog_slug, post_slug, user_info) do
Manager.broadcast(
blog_editors_topic(blog_slug),
{:editor_joined, post_slug, user_info}
)
end
@doc """
Broadcasts that a user stopped editing a post.
"""
def broadcast_editor_left(blog_slug, post_slug, user_info) do
Manager.broadcast(
blog_editors_topic(blog_slug),
{:editor_left, post_slug, user_info}
)
end
# ============================================================================
# Bulk Operations Progress
# ============================================================================
@doc """
Returns the topic for bulk operation progress.
"""
def bulk_operation_topic(blog_slug) do
"#{@topic_prefix}:#{blog_slug}:bulk_operations"
end
@doc """
Subscribes to bulk operation progress for a blog.
"""
def subscribe_to_bulk_operations(blog_slug) do
Manager.subscribe(bulk_operation_topic(blog_slug))
end
@doc """
Broadcasts bulk operation progress.
"""
def broadcast_bulk_operation_progress(blog_slug, operation_id, operation_type, completed, total) do
Manager.broadcast(
bulk_operation_topic(blog_slug),
{:bulk_operation_progress, operation_id, operation_type, completed, total}
)
end
@doc """
Broadcasts bulk operation completion.
"""
def broadcast_bulk_operation_completed(blog_slug, operation_id, operation_type, results) do
Manager.broadcast(
bulk_operation_topic(blog_slug),
{:bulk_operation_completed, operation_id, operation_type, results}
)
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