0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/lib/application_layout_preloader.rb
David Taylor d42c648fc7
PERF: Move theme settings from js bundle to preload data (#40137)
This means that we no longer need to recompile theme JS bundles when
theme settings are changed. It has parity with site settings, which are
already included in the preload data. Theme#cached_settings is already
cached in-memory, so performance impact is minimal.

Since theme JS bundles are now only dependent on `extra_js` ThemeFields,
we can make their caching much more stricter, meaning that they don't
need to be recompiled on every single `theme.save!`. Now they will only
be invalidated when extra_js fields actually change, or when the theme
compiler_version changes.
2026-06-08 16:04:56 +01:00

165 lines
4.8 KiB
Ruby
Vendored

# frozen_string_literal: true
class ApplicationLayoutPreloader
include ReadOnlyMixin
def self.banner_json_cache
@banner_json_cache ||= DistributedCache.new("banner_json")
end
def initialize(guardian:, theme_id:, theme_target:, login_method:)
@guardian = guardian
@theme_id = theme_id
@theme_target = theme_target
@login_method = login_method
@preloaded = {}
end
def store_preloaded(key, json)
# I dislike that there is a gsub as opposed to a gsub!
# but we can not be mucking with user input, I wonder if there is a way
# to inject this safety deeper in the library or even in AM serializer
@preloaded[key] = json.gsub("</", "<\\/")
end
def preloaded_data
preload_anonymous_data
preload_upcoming_change_data(@guardian.user)
if @guardian.authenticated?
@guardian.user.sync_notification_channel_position
preload_current_user_data
end
@preloaded
end
def banner_json
return "{}" if !@guardian.authenticated? && SiteSetting.login_required?
self
.class
.banner_json_cache
.defer_get_set("json_#{I18n.locale}") do
topic = Topic.where(archetype: Archetype.banner).first
banner = topic.present? && !topic.category&.read_restricted? ? topic.banner(@guardian) : {}
MultiJson.dump(banner)
end
end
def custom_html_json
data =
if @theme_id.present?
{
top: Theme.lookup_field(@theme_id, @theme_target, "after_header"),
footer: Theme.lookup_field(@theme_id, @theme_target, "footer"),
}
else
{}
end
data.merge! DiscoursePluginRegistry.custom_html if DiscoursePluginRegistry.custom_html
DiscoursePluginRegistry.html_builders.each do |name, _|
if name.start_with?("client:")
data[name.sub(/\Aclient:/, "")] = DiscoursePluginRegistry.build_html(name, self)
end
end
MultiJson.dump(data)
end
private
def preload_current_user_data
@preloaded["currentUser"] = MultiJson.dump(
CurrentUserSerializer.new(
@guardian.user,
scope: @guardian,
root: false,
login_method: @login_method,
),
)
report = TopicTrackingState.report(@guardian.user)
serializer = TopicTrackingStateSerializer.new(report, scope: @guardian, root: false)
hash = serializer.as_json
@preloaded["topicTrackingStates"] = MultiJson.dump(hash[:data])
@preloaded["topicTrackingStateMeta"] = MultiJson.dump(hash[:meta])
if @guardian.is_admin?
# This is used in the wizard so we can preload fonts using the FontMap JS API.
@preloaded["fontMap"] = MultiJson.dump(load_font_map)
# Used to show plugin-specific admin routes in the sidebar.
@preloaded["visiblePlugins"] = MultiJson.dump(
Discourse
.plugins_sorted_by_name(enabled_only: false)
.map do |plugin|
{
name: plugin.name.downcase,
humanized_name: plugin.humanized_name,
admin_route: plugin.full_admin_route,
enabled: plugin.enabled?,
description: plugin.metadata.about,
}
end,
)
end
end
def preload_anonymous_data
check_readonly_mode if @readonly_mode.nil?
@preloaded["site"] = Site.json_for(@guardian)
@preloaded["siteSettings"] = SiteSetting.client_settings_json
@preloaded["themeSiteSettingOverrides"] = SiteSetting.theme_site_settings_json(@theme_id)
@preloaded["customHTML"] = custom_html_json
@preloaded["banner"] = banner_json
@preloaded["customEmoji"] = custom_emoji
@preloaded["isReadOnly"] = @readonly_mode.to_json
@preloaded["isStaffWritesOnly"] = @staff_writes_only_mode.to_json
@preloaded["activatedThemes"] = activated_themes_json
end
def preload_upcoming_change_data(user)
@preloaded["upcomingChanges"] = SiteSetting
.upcoming_change_site_settings
.each_with_object({}) do |upcoming_change, hash|
hash[upcoming_change] = UpcomingChanges.enabled_for_user?(upcoming_change, user)
end
.to_json
end
def activated_themes_json
id = @theme_id
return "{}" if id.blank?
ids = Theme.transform_ids(id)
Theme
.where(id: ids)
.each_with_object({}) do |theme, hash|
hash[theme.id] = { name: theme.name, settings: theme.cached_settings }
end
.to_json
end
def load_font_map
DiscourseFonts
.fonts
.each_with_object({}) do |font, font_map|
next if !font[:variants]
font_map[font[:key]] = font[:variants].map do |v|
{
url: "#{Discourse.base_url}/fonts/#{v[:filename]}?v=#{DiscourseFonts::VERSION}",
weight: v[:weight],
}
end
end
end
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
end