@@ -15,46 +15,68 @@ static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
1515
1616namespace photostructure ::sqlite {
1717
18- // Static member definitions for ValueStorage
19- std::unordered_map<int32_t , Napi::Reference<Napi::Value>>
20- ValueStorage::storage_;
21- std::mutex ValueStorage::mutex_;
22- std::atomic<int32_t > ValueStorage::next_id_{0 };
23-
2418// ValueStorage implementation
25- int32_t ValueStorage::Store ([[maybe_unused]] Napi::Env env, Napi::Value value) {
26- const std::lock_guard<std::mutex> lock (mutex_);
27- const int32_t id = ++next_id_;
28-
29- // Try direct napi_value persistence instead of Napi::Reference
19+ int32_t ValueStorage::Store (Napi::Env env, Napi::Value value) {
20+ AddonData *addon_data = GetAddonData (env);
21+ if (!addon_data)
22+ throw Napi::Error::New (env, " Addon data not found" );
23+
24+ std::lock_guard<std::mutex> lock (addon_data->value_storage_mutex );
25+ const int32_t id =
26+ addon_data->next_value_id .fetch_add (1 , std::memory_order_relaxed);
3027 try {
31- storage_ [id] = Napi::Reference<Napi::Value>::New (value, 1 );
28+ addon_data-> value_storage [id] = Napi::Reference<Napi::Value>::New (value, 1 );
3229 } catch (...) {
3330 // If Reference creation fails, throw to let caller handle
3431 throw ;
3532 }
36-
3733 return id;
3834}
3935
4036Napi::Value ValueStorage::Get (Napi::Env env, int32_t id) {
41- const std::lock_guard<std::mutex> lock (mutex_);
42- auto it = storage_.find (id);
43- return (it != storage_.end ()) ? it->second .Value () : env.Undefined ();
37+ AddonData *addon_data = GetAddonData (env);
38+ if (!addon_data)
39+ return env.Null ();
40+
41+ std::lock_guard<std::mutex> lock (addon_data->value_storage_mutex );
42+ auto it = addon_data->value_storage .find (id);
43+ if (it == addon_data->value_storage .end () || it->second .IsEmpty ()) {
44+ return env.Null ();
45+ }
46+ return it->second .Value ();
4447}
4548
46- void ValueStorage::Remove (int32_t id) {
47- const std::lock_guard<std::mutex> lock (mutex_);
48- auto it = storage_.find (id);
49- if (it != storage_.end ()) {
50- // Don't call Reset() explicitly - let the Napi::Reference destructor
51- // handle cleanup naturally when erased. Explicit Reset() during GC
52- // causes JIT corruption on Alpine/musl.
53- // See: nodejs/node-addon-api#660, P02-investigate-flaky-native-crashes.md
54- storage_.erase (it);
49+ void ValueStorage::Remove (Napi::Env env, int32_t id) {
50+ AddonData *addon_data = GetAddonData (env);
51+ if (!addon_data)
52+ return ;
53+
54+ std::lock_guard<std::mutex> lock (addon_data->value_storage_mutex );
55+ auto it = addon_data->value_storage .find (id);
56+ if (it != addon_data->value_storage .end ()) {
57+ // Don't call Reset() here - it's unsafe during environment teardown on
58+ // musl. The Cleanup() hook will reset all remaining references safely. For
59+ // refs removed during normal operation, they'll be cleaned up by GC.
60+ addon_data->value_storage .erase (it);
5561 }
5662}
5763
64+ void CustomAggregate::CleanupHook (void *arg) {
65+ // Called before environment teardown - safe to Reset() references here
66+ auto *self = static_cast <CustomAggregate *>(arg);
67+
68+ if (!self->start_fn_ .IsEmpty ())
69+ self->start_fn_ .Reset ();
70+ if (!self->object_ref_ .IsEmpty ())
71+ self->object_ref_ .Reset ();
72+ if (!self->step_fn_ .IsEmpty ())
73+ self->step_fn_ .Reset ();
74+ if (!self->inverse_fn_ .IsEmpty ())
75+ self->inverse_fn_ .Reset ();
76+ if (!self->result_fn_ .IsEmpty ())
77+ self->result_fn_ .Reset ();
78+ }
79+
5880CustomAggregate::CustomAggregate (Napi::Env env, DatabaseSync *db,
5981 bool use_bigint_args, Napi::Value start,
6082 Napi::Function step_fn,
@@ -99,14 +121,22 @@ CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
99121 result_fn_ = Napi::Reference<Napi::Function>::New (result_fn, 1 );
100122 }
101123
124+ // Register cleanup hook to Reset() references before environment teardown.
125+ // This is required for worker thread support per Node-API best practices.
126+ // See:
127+ // https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
128+ napi_add_env_cleanup_hook (env_, CleanupHook, this );
129+
102130 // Don't create async context immediately - we'll create it lazily if needed
103131 async_context_ = nullptr ;
104132}
105133
106134CustomAggregate::~CustomAggregate () {
107- // Let Napi::FunctionReference destructors handle cleanup naturally.
108- // Explicitly calling Reset() during GC causes JIT corruption on Alpine/musl.
109- // See: nodejs/node-addon-api#660, P02-investigate-flaky-native-crashes.md
135+ // Remove cleanup hook if still registered
136+ napi_remove_env_cleanup_hook (env_, CleanupHook, this );
137+
138+ // Don't call Reset() on references here - CleanupHook already handled them,
139+ // or the environment is being torn down and Reset() would be unsafe.
110140
111141 // Check if environment is still valid before N-API operations.
112142 napi_handle_scope scope;
@@ -132,7 +162,10 @@ void CustomAggregate::xInverse(sqlite3_context *ctx, int argc,
132162 xStepBase (ctx, argc, argv, &CustomAggregate::inverse_fn_);
133163}
134164
135- void CustomAggregate::xFinal (sqlite3_context *ctx) { xValueBase (ctx, true ); }
165+ void CustomAggregate::xFinal (sqlite3_context *ctx) {
166+ xValueBase (ctx, true );
167+ DestroyAggregateData (ctx);
168+ }
136169
137170void CustomAggregate::xValue (sqlite3_context *ctx) { xValueBase (ctx, false ); }
138171
@@ -556,13 +589,16 @@ CustomAggregate::GetAggregate(sqlite3_context *ctx) {
556589}
557590
558591void CustomAggregate::DestroyAggregateData (sqlite3_context *ctx) {
592+ CustomAggregate *self =
593+ static_cast <CustomAggregate *>(sqlite3_user_data (ctx));
559594 AggregateData *agg = static_cast <AggregateData *>(
560595 sqlite3_aggregate_context (ctx, sizeof (AggregateData)));
561596
562- if (agg && agg->initialized ) {
563- ValueStorage::Remove (agg->value_id );
564- agg->initialized = false ;
597+ if (!self || !agg || !agg->initialized ) {
598+ return ;
565599 }
600+ ValueStorage::Remove (self->env_ , agg->value_id );
601+ agg->initialized = false ;
566602}
567603
568604Napi::Value CustomAggregate::SqliteValueToJS (sqlite3_value *value) {
0 commit comments