Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
hot_reload.hpp
Go to the documentation of this file.
1#pragma once
2
4
5// iOS skip: choc::file::Watcher is implemented on top of macOS's
6// `FSEventStream*` APIs, which are unavailable on iOS. Hot reload is a
7// dev-time-only feature. Every desktop editor path (AU v2/v3, VST3,
8// CLAP, AAX, standalone) honours `enable_hot_reload`; the iOS AUv3 /
9// HostApp path passes the same flag, but there is no watcher to serve
10// it, so file-watch reload is inert there. To keep the `HotReloader`
11// symbol available to `scripted_ui.cpp` and any other caller, ship a
12// no-op stub class on iOS that satisfies the same interface but never
13// instantiates a `choc::file::Watcher`. `ScriptedUiSession` logs once
14// when the flag is requested on such a platform so a developer is never
15// left wondering why saving a file changes nothing. On-device editor
16// rebuilds still work through the watcher-free
17// `ViewBridge::poll_editor_reload()` path, which follows a DSP hot-swap
18// rather than a file change.
19#if defined(__APPLE__)
20#include <TargetConditionals.h>
21#endif
22#if !defined(TARGET_OS_IPHONE)
23#define TARGET_OS_IPHONE 0
24#endif
25
26#if !TARGET_OS_IPHONE
27#include <choc/platform/choc_FileWatcher.h>
28#endif
29
30#include <atomic>
31#include <cstdint>
32#include <filesystem>
33#include <functional>
34#include <memory>
35#include <mutex>
36#include <optional>
37#include <string>
38#include <string_view>
39#include <unordered_map>
40
41namespace pulp::view {
42
43// Callback invoked when a JS file changes and is ready to be reloaded
44using ReloadCallback = std::function<void(const std::string& code)>;
45
46// Watches JS UI files for changes and triggers hot-reload
47// File changes are detected on a background thread; the reload callback
48// is stored for the UI thread to pick up via poll_reload()
50public:
51 // False on platforms that ship the no-op stub (no file watcher available),
52 // so callers can report the degradation instead of silently doing nothing.
53 // Prefer this over re-testing TARGET_OS_IPHONE at the call site.
54 static constexpr bool kWatchesFiles = !TARGET_OS_IPHONE;
55
56 // Watch a specific JS file
57 HotReloader(const std::filesystem::path& js_file, ReloadCallback on_reload);
58
59 // Watch a directory for .js file changes
60 HotReloader(const std::filesystem::path& directory,
61 const std::string& entry_file,
62 ReloadCallback on_reload);
63
65
66 HotReloader(const HotReloader&) = delete;
68
69 // Call from the UI thread to check if a reload is pending
70 // Returns true if a reload happened
72
73 // Get the path being watched
74 const std::filesystem::path& watched_path() const { return watched_path_; }
75
76 // How many reloads have occurred
77 uint32_t reload_count() const { return reload_count_.load(); }
78
79 // Whether the watcher has queued code that the UI thread has not applied.
80 bool has_pending_reload() const;
81
82 // Content-addressed reload gate: returns true only when `path` is a readable
83 // .js/.mjs whose content hash differs from the last observed hash (a
84 // save-without-edit or an editor's atomic-rename touch does not reload).
85 // Updates the observed-hash map as a side effect, under `hashes_mutex_`,
86 // so a caller on the owning thread may race the watcher thread safely. The
87 // gate is consume-once: of two callers seeing the same new content, exactly
88 // one is told to reload. Public because it is
89 // exercised directly by the unit tests: a private definition that is
90 // referenced from only one in-library call site is elided by MSVC (a private
91 // method cannot be referenced across a translation unit), so a test reaching
92 // it via `#define private public` links on Clang/GCC but fails on MSVC with
93 // an unresolved external. Exposing it guarantees an out-of-line symbol on
94 // every toolchain.
95 bool should_reload_for_modified_file(const std::filesystem::path& path);
96
97private:
98 std::filesystem::path watched_path_;
99 std::string entry_file_;
100 ReloadCallback on_reload_;
101#if !TARGET_OS_IPHONE
102 // The watcher owns a background thread that dispatches into
103 // `on_file_changed`, which writes every member below. Only `~Watcher`
104 // stops that thread, so the destructor releases this explicitly before
105 // anything it feeds. Reverse-declaration-order alone would tear the
106 // watched state down first and leave the thread writing into it.
107 std::unique_ptr<choc::file::Watcher> watcher_;
108#endif
109 // Guards `observed_content_hashes_`. Separate from `pending_mutex_`
110 // because the content gate reads the file before it touches the map, and
111 // holding the pending-code lock across that I/O would stall `poll_reload`.
112 mutable std::mutex hashes_mutex_;
113 std::unordered_map<std::string, std::uint64_t> observed_content_hashes_;
114
115 mutable std::mutex pending_mutex_;
116 std::string pending_code_;
117 bool has_pending_ = false;
118 std::atomic<uint32_t> reload_count_{0};
119
120#if !TARGET_OS_IPHONE
121 void on_file_changed(const choc::file::Watcher::Event& event);
122#endif
123 std::optional<std::string> try_read_file(const std::filesystem::path& path);
124 std::string read_file(const std::filesystem::path& path);
125 void seed_observed_content_hashes();
126 static std::uint64_t content_hash(std::string_view content);
127};
128
129} // namespace pulp::view
static constexpr bool kWatchesFiles
Definition hot_reload.hpp:54
HotReloader(const std::filesystem::path &js_file, ReloadCallback on_reload)
HotReloader & operator=(const HotReloader &)=delete
const std::filesystem::path & watched_path() const
Definition hot_reload.hpp:74
bool has_pending_reload() const
uint32_t reload_count() const
Definition hot_reload.hpp:77
HotReloader(const HotReloader &)=delete
bool should_reload_for_modified_file(const std::filesystem::path &path)
HotReloader(const std::filesystem::path &directory, const std::string &entry_file, ReloadCallback on_reload)
#define TARGET_OS_IPHONE
Definition hot_reload.hpp:23
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ path
Definition anchor_strategy.hpp:42
std::function< void(const std::string &code)> ReloadCallback
Definition hot_reload.hpp:44