44#include < node.h>
55#include < node_version.h>
66#include < optional>
7+ #include < vector>
78
89// Platform-specific includes for time functions
910#ifdef _WIN32
@@ -30,8 +31,9 @@ static const int kMaxStackFrames = 50;
3031struct AsyncLocalStorageLookup {
3132 // Async local storage instance associated with this thread
3233 v8::Global<v8::Value> async_local_storage;
33- // Optional key used to look up specific data in an async local storage object
34- std::optional<v8::Global<v8::Value>> storage_key;
34+ // Optional ordered array of keys (string | symbol) to traverse nested
35+ // Map/Object structures to fetch the final state object
36+ std::optional<std::vector<v8::Global<v8::Value>>> storage_keys;
3537};
3638
3739// Structure to hold information for each thread/isolate
@@ -75,11 +77,126 @@ struct ThreadResult {
7577 std::string poll_state;
7678};
7779
78- std::string JSONStringify (Isolate *isolate, Local<Value> value) {
79- HandleScope handle_scope (isolate);
80+ // Recursively sanitize a value to be safely JSON-stringifiable by:
81+ // - Removing properties whose values are BigInt, Function, or Symbol
82+ // (dropped for objects, omitted from arrays)
83+ // - Breaking cycles by omitting repeated objects (undefined -> dropped/omitted)
84+ // - Preserving primitives and traversing arrays/objects
85+ static v8::Local<v8::Value>
86+ SanitizeForJSON (v8::Isolate *isolate, v8::Local<v8::Context> context,
87+ v8::Local<v8::Value> value,
88+ std::vector<v8::Local<v8::Object>> &ancestors) {
89+ // Fast-path for primitives that are always JSON-compatible
90+ if (value->IsNull () || value->IsBoolean () || value->IsNumber () ||
91+ value->IsString ()) {
92+ return value;
93+ }
94+
95+ // Values that JSON.stringify cannot handle directly
96+ if (value->IsBigInt () || value->IsSymbol () || value->IsFunction () ||
97+ value->IsUndefined ()) {
98+ // Returning undefined here lets callers decide to drop (object) or null
99+ // (array)
100+ return v8::Undefined (isolate);
101+ }
102+
103+ // Arrays
104+ if (value->IsArray ()) {
105+ auto arr = value.As <v8::Array>();
106+ // Cycle detection
107+ auto arr_obj = value.As <v8::Object>();
108+ for (auto &a : ancestors) {
109+ if (a->StrictEquals (arr_obj)) {
110+ return v8::Undefined (isolate);
111+ }
112+ }
113+
114+ auto length = arr->Length ();
115+ auto out = v8::Array::New (isolate, 0 );
116+ ancestors.push_back (arr_obj);
117+
118+ uint32_t out_index = 0 ;
119+ for (uint32_t i = 0 ; i < length; ++i) {
120+ auto maybeEl = arr->Get (context, i);
121+ v8::Local<v8::Value> el =
122+ maybeEl.IsEmpty () ? v8::Undefined (isolate) : maybeEl.ToLocalChecked ();
123+
124+ auto sanitized = SanitizeForJSON (isolate, context, el, ancestors);
125+ if (!sanitized->IsUndefined ()) {
126+ out->Set (context, out_index++, sanitized)
127+ .Check (); // omit undefined entries entirely
128+ }
129+ }
130+ ancestors.pop_back ();
131+ return out;
132+ }
133+
134+ // Objects (including Dates, RegExps, Maps as objects; we only traverse
135+ // enumerable own props)
136+ if (value->IsObject ()) {
137+ auto obj = value.As <v8::Object>();
138+ // Cycle detection
139+ for (auto &a : ancestors) {
140+ if (a->StrictEquals (obj)) {
141+ return v8::Undefined (isolate);
142+ }
143+ }
144+
145+ ancestors.push_back (obj);
146+
147+ // Collect own enumerable property names (string-keyed)
148+ auto maybe_props = obj->GetPropertyNames (context);
149+ if (maybe_props.IsEmpty ()) {
150+ ancestors.pop_back ();
151+ return obj; // Nothing enumerable to sanitize
152+ }
153+
154+ auto props = maybe_props.ToLocalChecked ();
155+ auto out = v8::Object::New (isolate);
156+ auto len = props->Length ();
157+ for (uint32_t i = 0 ; i < len; ++i) {
158+ auto maybeKey = props->Get (context, i);
159+ if (maybeKey.IsEmpty ())
160+ continue ;
161+
162+ auto key = maybeKey.ToLocalChecked ();
163+ if (!key->IsString ()) {
164+ // Skip symbol and non-string keys to match JSON behavior
165+ continue ;
166+ }
167+
168+ auto maybeVal = obj->Get (context, key);
169+ if (maybeVal.IsEmpty ())
170+ continue ;
171+
172+ auto val = maybeVal.ToLocalChecked ();
173+ auto sanitized = SanitizeForJSON (isolate, context, val, ancestors);
174+ if (!sanitized->IsUndefined ()) {
175+ out->Set (context, key, sanitized).Check ();
176+ }
177+ // else: undefined -> drop property
178+ }
179+
180+ ancestors.pop_back ();
181+ return out;
182+ }
183+
184+ // Fallback: return as-is (shouldn't hit here for other exotic types)
185+ return value;
186+ }
80187
188+ std::string JSONStringify (Isolate *isolate, Local<Value> value) {
81189 auto context = isolate->GetCurrentContext ();
82- auto maybe_json = v8::JSON::Stringify (context, value);
190+
191+ // Sanitize the value first to avoid JSON failures (e.g., BigInt, cycles)
192+ std::vector<v8::Local<v8::Object>> ancestors;
193+ auto sanitized = SanitizeForJSON (isolate, context, value, ancestors);
194+ if (sanitized->IsUndefined ()) {
195+ // Nothing serializable
196+ return " " ;
197+ }
198+
199+ auto maybe_json = v8::JSON::Stringify (context, sanitized);
83200 if (maybe_json.IsEmpty ()) {
84201 return " " ;
85202 }
@@ -89,8 +206,6 @@ std::string JSONStringify(Isolate *isolate, Local<Value> value) {
89206
90207// Function to get stack frames from a V8 stack trace
91208JsStackFrames GetStackFrames (Isolate *isolate) {
92- HandleScope handle_scope (isolate);
93-
94209 auto stack = StackTrace::CurrentStackTrace (isolate, kMaxStackFrames ,
95210 StackTrace::kDetailed );
96211
@@ -135,12 +250,11 @@ JsStackFrames GetStackFrames(Isolate *isolate) {
135250// Function to fetch the thread state from the async context store
136251std::string GetThreadState (Isolate *isolate,
137252 const AsyncLocalStorageLookup &store) {
138- HandleScope handle_scope (isolate);
139253
140- // Node.js stores the async local storage in the isolate's
141- // "ContinuationPreservedEmbedderData" map, keyed by the
142- // AsyncLocalStorage instance.
143- // https://github.com/nodejs/node/blob/c6316f9db9869864cea84e5f07585fa08e3e06d2/src/async_context_frame.cc#L37
254+ // Node.js stores the async local storage in the isolate's
255+ // "ContinuationPreservedEmbedderData" map, keyed by the
256+ // AsyncLocalStorage instance.
257+ // https://github.com/nodejs/node/blob/c6316f9db9869864cea84e5f07585fa08e3e06d2/src/async_context_frame.cc#L37
144258#if GET_CONTINUATION_PRESERVED_EMBEDDER_DATA_V2
145259 auto data = isolate->GetContinuationPreservedEmbedderDataV2 ().As <Value>();
146260#else
@@ -162,18 +276,36 @@ std::string GetThreadState(Isolate *isolate,
162276
163277 auto root_store = maybe_root_store.ToLocalChecked ();
164278
165- if (store.storage_key .has_value () && root_store->IsObject ()) {
166- auto local_key = store.storage_key ->Get (isolate);
279+ if (store.storage_keys .has_value ()) {
280+ // Walk the keys to get the desired nested value
281+ const auto &keys = store.storage_keys .value ();
282+ auto current = root_store;
283+
284+ for (auto &gkey : keys) {
285+ auto local_key = gkey.Get (isolate);
286+ if (!(local_key->IsString () || local_key->IsSymbol ())) {
287+ continue ;
288+ }
289+
290+ v8::MaybeLocal<v8::Value> maybeValue;
291+ if (current->IsMap ()) {
292+ auto map_val = current.As <v8::Map>();
293+ maybeValue = map_val->Get (context, local_key);
294+ } else if (current->IsObject ()) {
295+ auto obj_val = current.As <v8::Object>();
296+ maybeValue = obj_val->Get (context, local_key);
297+ } else {
298+ return " " ;
299+ }
167300
168- if (local_key->IsString () || local_key->IsSymbol ()) {
169- auto root_obj = root_store.As <v8::Object>();
170- auto maybeValue = root_obj->Get (context, local_key);
171301 if (maybeValue.IsEmpty ()) {
172302 return " " ;
173303 }
174304
175- root_store = maybeValue.ToLocalChecked ();
305+ current = maybeValue.ToLocalChecked ();
176306 }
307+
308+ root_store = current;
177309 }
178310
179311 return JSONStringify (isolate, root_store);
@@ -232,7 +364,6 @@ CaptureStackTrace(Isolate *isolate,
232364// Function to capture stack traces from all registered threads
233365void CaptureStackTraces (const FunctionCallbackInfo<Value> &args) {
234366 auto capture_from_isolate = args.GetIsolate ();
235- HandleScope handle_scope (capture_from_isolate);
236367
237368 std::vector<ThreadResult> results;
238369
@@ -401,7 +532,6 @@ void RegisterThreadInternal(
401532// Function to register a thread and update its last seen time
402533void RegisterThread (const FunctionCallbackInfo<Value> &args) {
403534 auto isolate = args.GetIsolate ();
404- HandleScope handle_scope (isolate);
405535
406536 if (args.Length () == 1 && args[0 ]->IsString ()) {
407537 v8::String::Utf8Value utf8 (isolate, args[0 ]);
@@ -430,24 +560,45 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
430560 return ;
431561 }
432562
433- std::optional<v8::Global<v8::Value>> storage_key = std::nullopt ;
563+ std::optional<std::vector<v8::Global<v8::Value>>> storage_keys =
564+ std::nullopt ;
434565
435- auto storage_key_val = obj->Get (
436- isolate->GetCurrentContext (),
437- String::NewFromUtf8 (isolate, " storageKey" , NewStringType::kInternalized )
438- .ToLocalChecked ());
566+ auto storage_key_val =
567+ obj->Get (isolate->GetCurrentContext (),
568+ String::NewFromUtf8 (isolate, " stateLookup" ,
569+ NewStringType::kInternalized )
570+ .ToLocalChecked ());
439571
440572 if (!storage_key_val.IsEmpty ()) {
573+
441574 auto local_val = storage_key_val.ToLocalChecked ();
442575 if (!local_val->IsUndefined () && !local_val->IsNull ()) {
443- storage_key = v8::Global<v8::Value>(isolate, local_val);
576+ if (local_val->IsArray ()) {
577+
578+ auto arr = local_val.As <v8::Array>();
579+ std::vector<v8::Global<v8::Value>> keys_vec;
580+ uint32_t length = arr->Length ();
581+ for (uint32_t i = 0 ; i < length; ++i) {
582+ auto maybeEl = arr->Get (isolate->GetCurrentContext (), i);
583+ if (maybeEl.IsEmpty ())
584+ continue ;
585+ auto el = maybeEl.ToLocalChecked ();
586+ if (el->IsString () || el->IsSymbol ()) {
587+
588+ keys_vec.emplace_back (isolate, el);
589+ }
590+ }
591+ if (!keys_vec.empty ()) {
592+ storage_keys = std::move (keys_vec);
593+ }
594+ }
444595 }
445596 }
446597
447598 auto store = AsyncLocalStorageLookup{
448599 v8::Global<v8::Value>(isolate,
449600 async_local_storage_val.ToLocalChecked ()),
450- std::move (storage_key )};
601+ std::move (storage_keys )};
451602
452603 RegisterThreadInternal (isolate, thread_name, std::move (store));
453604 } else {
@@ -457,7 +608,8 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
457608 " Incorrect arguments. Expected: \n "
458609 " - registerThread(threadName: string) or \n "
459610 " - registerThread(storage: {asyncLocalStorage: AsyncLocalStorage; "
460- " storageKey?: string | symbol}, threadName: string)" ,
611+ " stateLookup?: Array<string | symbol>}, "
612+ " threadName: string)" ,
461613 NewStringType::kInternalized )
462614 .ToLocalChecked ()));
463615 }
@@ -491,7 +643,6 @@ steady_clock::time_point GetUnbiasedMonotonicTime() {
491643// Function to track a thread and set its state
492644void ThreadPoll (const FunctionCallbackInfo<Value> &args) {
493645 auto isolate = args.GetIsolate ();
494- HandleScope handle_scope (isolate);
495646
496647 bool enable_last_seen = true ;
497648 if (args.Length () > 0 && args[0 ]->IsBoolean ()) {
@@ -524,7 +675,6 @@ void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
524675// Function to get the last seen time of all registered threads
525676void GetThreadsLastSeen (const FunctionCallbackInfo<Value> &args) {
526677 Isolate *isolate = args.GetIsolate ();
527- HandleScope handle_scope (isolate);
528678
529679 Local<Object> result = Object::New (isolate);
530680 milliseconds now = duration_cast<milliseconds>(
0 commit comments