@@ -29,6 +29,10 @@ static inline bool EndsWith(const std::string& s, const char* suffix) {
2929static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotAccept;
3030static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotDispose;
3131
32+ // Custom event listeners
33+ // Keyed by event name (global, not per-module)
34+ static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotEventListeners;
35+
3236v8::Local<v8::Object> GetOrCreateHotData (v8::Isolate* isolate, const std::string& key) {
3337 auto it = g_hotData.find (key);
3438 if (it != g_hotData.end ()) {
@@ -73,6 +77,76 @@ void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local<
7377 return out;
7478}
7579
80+ void RegisterHotEventListener (v8::Isolate* isolate, const std::string& event, v8::Local<v8::Function> cb) {
81+ if (cb.IsEmpty ()) return ;
82+ g_hotEventListeners[event].emplace_back (v8::Global<v8::Function>(isolate, cb));
83+ }
84+
85+ std::vector<v8::Local<v8::Function>> GetHotEventListeners (v8::Isolate* isolate, const std::string& event) {
86+ std::vector<v8::Local<v8::Function>> out;
87+ auto it = g_hotEventListeners.find (event);
88+ if (it != g_hotEventListeners.end ()) {
89+ for (auto & gfn : it->second ) {
90+ if (!gfn.IsEmpty ()) out.push_back (gfn.Get (isolate));
91+ }
92+ }
93+ return out;
94+ }
95+
96+ void DispatchHotEvent (v8::Isolate* isolate, v8::Local<v8::Context> context, const std::string& event, v8::Local<v8::Value> data) {
97+ auto callbacks = GetHotEventListeners (isolate, event);
98+ for (auto & cb : callbacks) {
99+ v8::TryCatch tryCatch (isolate);
100+ v8::Local<v8::Value> args[] = { data };
101+ v8::MaybeLocal<v8::Value> result = cb->Call (context, v8::Undefined (isolate), 1 , args);
102+ (void )result; // Suppress unused result warning
103+ if (tryCatch.HasCaught ()) {
104+ // Log error but continue to other listeners
105+ if (tns::IsScriptLoadingLogEnabled ()) {
106+ Log (@" [import.meta.hot] Error in event listener for '%s '" , event.c_str ());
107+ }
108+ }
109+ }
110+ }
111+
112+ void InitializeHotEventDispatcher (v8::Isolate* isolate, v8::Local<v8::Context> context) {
113+ using v8::FunctionCallbackInfo;
114+ using v8::Local;
115+ using v8::Value;
116+
117+ // Create a global function __NS_DISPATCH_HOT_EVENT__(event, data)
118+ // that the HMR client can call to dispatch events to registered listeners
119+ auto dispatchCb = [](const FunctionCallbackInfo<Value>& info) {
120+ v8::Isolate* iso = info.GetIsolate ();
121+ v8::Local<v8::Context> ctx = iso->GetCurrentContext ();
122+
123+ if (info.Length () < 1 || !info[0 ]->IsString ()) {
124+ info.GetReturnValue ().Set (v8::Boolean::New (iso, false ));
125+ return ;
126+ }
127+
128+ v8::String::Utf8Value eventName (iso, info[0 ]);
129+ std::string event = *eventName ? *eventName : " " ;
130+ if (event.empty ()) {
131+ info.GetReturnValue ().Set (v8::Boolean::New (iso, false ));
132+ return ;
133+ }
134+
135+ v8::Local<Value> data = info.Length () > 1 ? info[1 ] : v8::Undefined (iso).As <Value>();
136+
137+ if (tns::IsScriptLoadingLogEnabled ()) {
138+ Log (@" [import.meta.hot] Dispatching event '%s '" , event.c_str ());
139+ }
140+
141+ DispatchHotEvent (iso, ctx, event, data);
142+ info.GetReturnValue ().Set (v8::Boolean::New (iso, true ));
143+ };
144+
145+ v8::Local<v8::Object> global = context->Global ();
146+ v8::Local<v8::Function> dispatchFn = v8::Function::New (context, dispatchCb).ToLocalChecked ();
147+ global->CreateDataProperty (context, tns::ToV8String (isolate, " __NS_DISPATCH_HOT_EVENT__" ), dispatchFn).Check ();
148+ }
149+
76150void InitializeImportMetaHot (v8::Isolate* isolate,
77151 v8::Local<v8::Context> context,
78152 v8::Local<v8::Object> importMeta,
@@ -194,6 +268,31 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
194268 info.GetReturnValue ().Set (v8::Undefined (info.GetIsolate ()));
195269 };
196270
271+ // on(event, cb) — register custom event listener
272+ auto onCb = [](const FunctionCallbackInfo<Value>& info) {
273+ v8::Isolate* iso = info.GetIsolate ();
274+ if (info.Length () < 2 ) {
275+ info.GetReturnValue ().Set (v8::Undefined (iso));
276+ return ;
277+ }
278+ if (!info[0 ]->IsString () || !info[1 ]->IsFunction ()) {
279+ info.GetReturnValue ().Set (v8::Undefined (iso));
280+ return ;
281+ }
282+ v8::String::Utf8Value eventName (iso, info[0 ]);
283+ std::string event = *eventName ? *eventName : " " ;
284+ if (!event.empty ()) {
285+ RegisterHotEventListener (iso, event, info[1 ].As <v8::Function>());
286+ }
287+ info.GetReturnValue ().Set (v8::Undefined (iso));
288+ };
289+
290+ // send(event, data) — send event to server (no-op on client, could be wired to WebSocket)
291+ auto sendCb = [](const FunctionCallbackInfo<Value>& info) {
292+ // No-op for now - could be wired to WebSocket for client->server events
293+ info.GetReturnValue ().Set (v8::Undefined (info.GetIsolate ()));
294+ };
295+
197296 Local<Object> hot = Object::New (isolate);
198297 // Stable flags
199298 hot->CreateDataProperty (context, tns::ToV8String (isolate, " data" ),
@@ -213,6 +312,12 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
213312 hot->CreateDataProperty (
214313 context, tns::ToV8String (isolate, " invalidate" ),
215314 v8::Function::New (context, invalidateCb, makeKeyData (key)).ToLocalChecked ()).Check ();
315+ hot->CreateDataProperty (
316+ context, tns::ToV8String (isolate, " on" ),
317+ v8::Function::New (context, onCb, makeKeyData (key)).ToLocalChecked ()).Check ();
318+ hot->CreateDataProperty (
319+ context, tns::ToV8String (isolate, " send" ),
320+ v8::Function::New (context, sendCb, makeKeyData (key)).ToLocalChecked ()).Check ();
216321
217322 // Attach to import.meta
218323 importMeta->CreateDataProperty (
0 commit comments