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,9 +77,126 @@ struct ThreadResult {
7577 std::string poll_state;
7678};
7779
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+ }
187+
78188std::string JSONStringify (Isolate *isolate, Local<Value> value) {
79189 auto context = isolate->GetCurrentContext ();
80- 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);
81200 if (maybe_json.IsEmpty ()) {
82201 return " " ;
83202 }
@@ -131,10 +250,11 @@ JsStackFrames GetStackFrames(Isolate *isolate) {
131250// Function to fetch the thread state from the async context store
132251std::string GetThreadState (Isolate *isolate,
133252 const AsyncLocalStorageLookup &store) {
134- // Node.js stores the async local storage in the isolate's
135- // "ContinuationPreservedEmbedderData" map, keyed by the
136- // AsyncLocalStorage instance.
137- // https://github.com/nodejs/node/blob/c6316f9db9869864cea84e5f07585fa08e3e06d2/src/async_context_frame.cc#L37
253+
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
138258#if GET_CONTINUATION_PRESERVED_EMBEDDER_DATA_V2
139259 auto data = isolate->GetContinuationPreservedEmbedderDataV2 ().As <Value>();
140260#else
@@ -156,18 +276,36 @@ std::string GetThreadState(Isolate *isolate,
156276
157277 auto root_store = maybe_root_store.ToLocalChecked ();
158278
159- if (store.storage_key .has_value () && root_store->IsObject ()) {
160- 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+ }
161300
162- if (local_key->IsString () || local_key->IsSymbol ()) {
163- auto root_obj = root_store.As <v8::Object>();
164- auto maybeValue = root_obj->Get (context, local_key);
165301 if (maybeValue.IsEmpty ()) {
166302 return " " ;
167303 }
168304
169- root_store = maybeValue.ToLocalChecked ();
305+ current = maybeValue.ToLocalChecked ();
170306 }
307+
308+ root_store = current;
171309 }
172310
173311 return JSONStringify (isolate, root_store);
@@ -422,24 +560,45 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
422560 return ;
423561 }
424562
425- std::optional<v8::Global<v8::Value>> storage_key = std::nullopt ;
563+ std::optional<std::vector<v8::Global<v8::Value>>> storage_keys =
564+ std::nullopt ;
426565
427- auto storage_key_val = obj->Get (
428- isolate->GetCurrentContext (),
429- String::NewFromUtf8 (isolate, " storageKey" , NewStringType::kInternalized )
430- .ToLocalChecked ());
566+ auto storage_key_val =
567+ obj->Get (isolate->GetCurrentContext (),
568+ String::NewFromUtf8 (isolate, " stateLookup" ,
569+ NewStringType::kInternalized )
570+ .ToLocalChecked ());
431571
432572 if (!storage_key_val.IsEmpty ()) {
573+
433574 auto local_val = storage_key_val.ToLocalChecked ();
434575 if (!local_val->IsUndefined () && !local_val->IsNull ()) {
435- 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+ }
436595 }
437596 }
438597
439598 auto store = AsyncLocalStorageLookup{
440599 v8::Global<v8::Value>(isolate,
441600 async_local_storage_val.ToLocalChecked ()),
442- std::move (storage_key )};
601+ std::move (storage_keys )};
443602
444603 RegisterThreadInternal (isolate, thread_name, std::move (store));
445604 } else {
@@ -449,7 +608,8 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
449608 " Incorrect arguments. Expected: \n "
450609 " - registerThread(threadName: string) or \n "
451610 " - registerThread(storage: {asyncLocalStorage: AsyncLocalStorage; "
452- " storageKey?: string | symbol}, threadName: string)" ,
611+ " stateLookup?: Array<string | symbol>}, "
612+ " threadName: string)" ,
453613 NewStringType::kInternalized )
454614 .ToLocalChecked ()));
455615 }
0 commit comments