@homaiohq/napi-quickfix 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -185,6 +185,14 @@ void AcceptorWrap::DestroyStoppedEngine() {
185
185
  SessionOpGate::Unregister(sessionIDs_, gate_.get());
186
186
  acceptor_.reset();
187
187
  gate_->Unfreeze();
188
+ // The engine is gone, so nothing will call the handlers again: drop the
189
+ // bridge's reference to them. It is what kept this wrap reachable from the
190
+ // user's Engine object (the handlers close over it) and so made the wrap
191
+ // -- and the Engine, its listeners and the parsed dictionaries -- immortal.
192
+ // On the graceful path this runs from the barrier queued behind every
193
+ // callback stop() fired, so none of them is lost; on the force path the
194
+ // TSFN was aborted first, discarding the queue.
195
+ if (bridge_) bridge_->DropHandlers();
188
196
  }
189
197
 
190
198
  Napi::Value AcceptorWrap::Start(const Napi::CallbackInfo& info) {
@@ -248,9 +256,13 @@ Napi::Value AcceptorWrap::Stop(const Napi::CallbackInfo& info) {
248
256
  return deferred.Promise();
249
257
  }
250
258
  if (!acceptor_) {
251
- // Already torn down: just release the TSFN and resolve.
259
+ // Already torn down: just release the TSFN, drop the handlers (nothing
260
+ // can call them without an engine) and resolve.
252
261
  stopped_ = true;
253
- if (bridge_) bridge_->Release();
262
+ if (bridge_) {
263
+ bridge_->Release();
264
+ bridge_->DropHandlers();
265
+ }
254
266
  deferred.Resolve(env.Undefined());
255
267
  return deferred.Promise();
256
268
  }
@@ -323,8 +335,15 @@ Napi::Value AcceptorWrap::Stop(const Napi::CallbackInfo& info) {
323
335
  self->gate_->Unfreeze();
324
336
  if (bridge) {
325
337
  if (force) {
338
+ // The abort discarded the queue: no callback can reach JS
339
+ // any more, so the handlers can go now.
326
340
  bridge->Abort();
341
+ bridge->DropHandlers();
327
342
  } else {
343
+ // Callbacks already queued (a trailing 'logout') are still
344
+ // dispatched after the release, and read the handlers: keep
345
+ // them. The retried stop() drops them in
346
+ // DestroyStoppedEngine().
328
347
  bridge->Release();
329
348
  }
330
349
  }
package/cpp/addon.cpp CHANGED
@@ -30,7 +30,7 @@ void RegisterSessionStatic(Napi::Env env, Napi::Object exports);
30
30
  namespace {
31
31
 
32
32
  // Keep in sync with package.json "version".
33
- constexpr const char* kAddonVersion = "0.3.0";
33
+ constexpr const char* kAddonVersion = "0.3.1";
34
34
 
35
35
  Napi::Value Version(const Napi::CallbackInfo& info) {
36
36
  return Napi::String::New(info.Env(), kAddonVersion);
@@ -167,6 +167,18 @@ void ApplicationBridge::WakeAllPending() {
167
167
  }
168
168
  }
169
169
 
170
+ void ApplicationBridge::DropHandlers() {
171
+ if (!handlers_.IsEmpty()) {
172
+ handlers_.Reset();
173
+ }
174
+ // The dictionaries pin a parsed FIX spec per session (a copy of the
175
+ // session's DataDictionaryProvider, which shares the dictionary's
176
+ // ownership); the sessions that could still want them are gone. Locked
177
+ // for form: the network threads that fill the map are joined by now.
178
+ std::lock_guard<std::mutex> lock(dictionariesMtx_);
179
+ dictionaries_.clear();
180
+ }
181
+
170
182
  void ApplicationBridge::Ref(Napi::Env env) {
171
183
  if (!released_) {
172
184
  tsfn_.Ref(env);
@@ -125,6 +125,13 @@ class ApplicationBridge : public FIX::Application {
125
125
  void Ref(Napi::Env env);
126
126
  void Unref(Napi::Env env);
127
127
 
128
+ // Drop what the bridge roots on the engine's behalf: the handlers object
129
+ // and the per-session dictionaries (see handlers_ below). JS thread only;
130
+ // call it once the engine is gone, or once no queued callback can reach JS
131
+ // any more (after Abort()). A callback dispatched afterwards passes
132
+ // through as if no handler were installed.
133
+ void DropHandlers();
134
+
128
135
  // Queue `fn` to run on the JS thread AFTER every callback already queued
129
136
  // (the TSFN queue is FIFO), and keep the event loop alive until it has run.
130
137
  // JS thread only; call it before Release()/Abort(). Returns false, without
@@ -167,6 +174,14 @@ class ApplicationBridge : public FIX::Application {
167
174
  std::map<FIX::SessionID, std::shared_ptr<const FIX::DataDictionary>>
168
175
  dictionaries_;
169
176
 
177
+ // The user's handlers object, held strongly for the engine's whole life so
178
+ // a started engine stays reachable through them (the wrapper's handlers
179
+ // close over the Engine), like a net.Server does through its listeners.
180
+ // That makes a reference cycle through native memory the GC cannot see:
181
+ // wrap -> bridge -> handlers -> Engine -> wrap. The wraps break it with
182
+ // DropHandlers() as soon as the engine is destroyed (stop() settled, or
183
+ // the force/failed-stop paths that abort the TSFN); the destructor's Reset
184
+ // is only the fallback for a wrap collected without stop().
170
185
  Napi::ObjectReference handlers_;
171
186
  TSFN tsfn_;
172
187
  bool released_ = false;
@@ -207,6 +207,14 @@ void InitiatorWrap::DestroyStoppedEngine() {
207
207
  SessionOpGate::Unregister(sessionIDs_, gate_.get());
208
208
  initiator_.reset();
209
209
  gate_->Unfreeze();
210
+ // The engine is gone, so nothing will call the handlers again: drop the
211
+ // bridge's reference to them. It is what kept this wrap reachable from the
212
+ // user's Engine object (the handlers close over it) and so made the wrap
213
+ // -- and the Engine, its listeners and the parsed dictionaries -- immortal.
214
+ // On the graceful path this runs from the barrier queued behind every
215
+ // callback stop() fired, so none of them is lost; on the force path the
216
+ // TSFN was aborted first, discarding the queue.
217
+ if (bridge_) bridge_->DropHandlers();
210
218
  }
211
219
 
212
220
  Napi::Value InitiatorWrap::Start(const Napi::CallbackInfo& info) {
@@ -272,9 +280,13 @@ Napi::Value InitiatorWrap::Stop(const Napi::CallbackInfo& info) {
272
280
  return deferred.Promise();
273
281
  }
274
282
  if (!initiator_) {
275
- // Already torn down: just release the TSFN and resolve.
283
+ // Already torn down: just release the TSFN, drop the handlers (nothing
284
+ // can call them without an engine) and resolve.
276
285
  stopped_ = true;
277
- if (bridge_) bridge_->Release();
286
+ if (bridge_) {
287
+ bridge_->Release();
288
+ bridge_->DropHandlers();
289
+ }
278
290
  deferred.Resolve(env.Undefined());
279
291
  return deferred.Promise();
280
292
  }
@@ -347,8 +359,15 @@ Napi::Value InitiatorWrap::Stop(const Napi::CallbackInfo& info) {
347
359
  self->gate_->Unfreeze();
348
360
  if (bridge) {
349
361
  if (force) {
362
+ // The abort discarded the queue: no callback can reach JS
363
+ // any more, so the handlers can go now.
350
364
  bridge->Abort();
365
+ bridge->DropHandlers();
351
366
  } else {
367
+ // Callbacks already queued (a trailing 'logout') are still
368
+ // dispatched after the release, and read the handlers: keep
369
+ // them. The retried stop() drops them in
370
+ // DestroyStoppedEngine().
352
371
  bridge->Release();
353
372
  }
354
373
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homaiohq/napi-quickfix",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Modern Node-API (N-API) C++ binding for the QuickFIX FIX-protocol engine.",
5
5
  "license": "MIT",
6
6
  "repository": {
Binary file
Binary file