@homaiohq/napi-quickfix 0.1.0 → 0.3.0

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.
Files changed (145) hide show
  1. package/CMakeLists.txt +5 -1
  2. package/README.md +344 -18
  3. package/cpp/acceptor_wrap.cpp +253 -19
  4. package/cpp/acceptor_wrap.h +60 -1
  5. package/cpp/addon.cpp +10 -6
  6. package/cpp/application_bridge.cpp +114 -30
  7. package/cpp/application_bridge.h +57 -4
  8. package/cpp/data_dictionary_wrap.cpp +89 -3
  9. package/cpp/data_dictionary_wrap.h +27 -5
  10. package/cpp/engine_common.h +108 -0
  11. package/cpp/engine_workers.h +99 -18
  12. package/cpp/field_map_ops.h +492 -0
  13. package/cpp/group_wrap.cpp +221 -0
  14. package/cpp/group_wrap.h +76 -0
  15. package/cpp/initiator_wrap.cpp +262 -25
  16. package/cpp/initiator_wrap.h +37 -2
  17. package/cpp/message_wrap.cpp +171 -78
  18. package/cpp/message_wrap.h +47 -5
  19. package/cpp/session_op_gate.h +201 -0
  20. package/cpp/session_static.cpp +139 -12
  21. package/cpp/session_wrap.cpp +438 -0
  22. package/cpp/session_wrap.h +144 -0
  23. package/dist/cjs/acceptor.d.ts +0 -1
  24. package/dist/cjs/acceptor.js +0 -1
  25. package/dist/cjs/application.d.ts +0 -1
  26. package/dist/cjs/application.js +0 -1
  27. package/dist/cjs/data-dictionary.d.ts +21 -3
  28. package/dist/cjs/data-dictionary.js +32 -4
  29. package/dist/cjs/engine.d.ts +42 -4
  30. package/dist/cjs/engine.js +50 -5
  31. package/dist/cjs/enums.d.ts +48 -14
  32. package/dist/cjs/enums.js +20 -25
  33. package/dist/cjs/field-value.d.ts +12 -0
  34. package/dist/cjs/field-value.js +40 -0
  35. package/dist/cjs/generated/fields.d.ts +0 -1
  36. package/dist/cjs/generated/fields.js +0 -1
  37. package/dist/cjs/generated/values.d.ts +8548 -0
  38. package/dist/cjs/generated/values.js +8580 -0
  39. package/dist/cjs/group.d.ts +126 -0
  40. package/dist/cjs/group.js +185 -0
  41. package/dist/cjs/index.d.ts +28 -5
  42. package/dist/cjs/index.js +30 -28
  43. package/dist/cjs/initiator.d.ts +0 -1
  44. package/dist/cjs/initiator.js +0 -1
  45. package/dist/cjs/message.d.ts +129 -5
  46. package/dist/cjs/message.js +146 -9
  47. package/dist/cjs/native.d.ts +179 -17
  48. package/dist/cjs/native.js +0 -1
  49. package/dist/cjs/session-id.d.ts +12 -1
  50. package/dist/cjs/session-id.js +21 -1
  51. package/dist/cjs/session-settings.d.ts +0 -1
  52. package/dist/cjs/session-settings.js +0 -1
  53. package/dist/cjs/session.d.ts +213 -0
  54. package/dist/cjs/session.js +337 -0
  55. package/dist/cjs/tls-tunnel.d.ts +143 -0
  56. package/dist/cjs/tls-tunnel.js +261 -0
  57. package/dist/esm/acceptor.d.ts +0 -1
  58. package/dist/esm/acceptor.js +0 -1
  59. package/dist/esm/application.d.ts +0 -1
  60. package/dist/esm/application.js +0 -1
  61. package/dist/esm/data-dictionary.d.ts +21 -3
  62. package/dist/esm/data-dictionary.js +32 -4
  63. package/dist/esm/engine.d.ts +42 -4
  64. package/dist/esm/engine.js +51 -6
  65. package/dist/esm/enums.d.ts +48 -14
  66. package/dist/esm/enums.js +14 -24
  67. package/dist/esm/field-value.d.ts +12 -0
  68. package/dist/esm/field-value.js +37 -0
  69. package/dist/esm/generated/fields.d.ts +0 -1
  70. package/dist/esm/generated/fields.js +0 -1
  71. package/dist/esm/generated/values.d.ts +8548 -0
  72. package/dist/esm/generated/values.js +8564 -0
  73. package/dist/esm/group.d.ts +126 -0
  74. package/dist/esm/group.js +181 -0
  75. package/dist/esm/index.d.ts +28 -5
  76. package/dist/esm/index.js +18 -26
  77. package/dist/esm/initiator.d.ts +0 -1
  78. package/dist/esm/initiator.js +0 -1
  79. package/dist/esm/message.d.ts +129 -5
  80. package/dist/esm/message.js +146 -9
  81. package/dist/esm/native.d.ts +179 -17
  82. package/dist/esm/native.js +0 -1
  83. package/dist/esm/session-id.d.ts +12 -1
  84. package/dist/esm/session-id.js +20 -1
  85. package/dist/esm/session-settings.d.ts +0 -1
  86. package/dist/esm/session-settings.js +0 -1
  87. package/dist/esm/session.d.ts +213 -0
  88. package/dist/esm/session.js +329 -0
  89. package/dist/esm/tls-tunnel.d.ts +143 -0
  90. package/dist/esm/tls-tunnel.js +256 -0
  91. package/package.json +13 -1
  92. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  93. package/prebuilds/darwin-x64/node.napi.node +0 -0
  94. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  95. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  96. package/prebuilds/win32-x64/node.napi.node +0 -0
  97. package/cpp/enums.cpp +0 -66
  98. package/dist/cjs/acceptor.d.ts.map +0 -1
  99. package/dist/cjs/acceptor.js.map +0 -1
  100. package/dist/cjs/application.d.ts.map +0 -1
  101. package/dist/cjs/application.js.map +0 -1
  102. package/dist/cjs/data-dictionary.d.ts.map +0 -1
  103. package/dist/cjs/data-dictionary.js.map +0 -1
  104. package/dist/cjs/engine.d.ts.map +0 -1
  105. package/dist/cjs/engine.js.map +0 -1
  106. package/dist/cjs/enums.d.ts.map +0 -1
  107. package/dist/cjs/enums.js.map +0 -1
  108. package/dist/cjs/generated/fields.d.ts.map +0 -1
  109. package/dist/cjs/generated/fields.js.map +0 -1
  110. package/dist/cjs/index.d.ts.map +0 -1
  111. package/dist/cjs/index.js.map +0 -1
  112. package/dist/cjs/initiator.d.ts.map +0 -1
  113. package/dist/cjs/initiator.js.map +0 -1
  114. package/dist/cjs/message.d.ts.map +0 -1
  115. package/dist/cjs/message.js.map +0 -1
  116. package/dist/cjs/native.d.ts.map +0 -1
  117. package/dist/cjs/native.js.map +0 -1
  118. package/dist/cjs/session-id.d.ts.map +0 -1
  119. package/dist/cjs/session-id.js.map +0 -1
  120. package/dist/cjs/session-settings.d.ts.map +0 -1
  121. package/dist/cjs/session-settings.js.map +0 -1
  122. package/dist/esm/acceptor.d.ts.map +0 -1
  123. package/dist/esm/acceptor.js.map +0 -1
  124. package/dist/esm/application.d.ts.map +0 -1
  125. package/dist/esm/application.js.map +0 -1
  126. package/dist/esm/data-dictionary.d.ts.map +0 -1
  127. package/dist/esm/data-dictionary.js.map +0 -1
  128. package/dist/esm/engine.d.ts.map +0 -1
  129. package/dist/esm/engine.js.map +0 -1
  130. package/dist/esm/enums.d.ts.map +0 -1
  131. package/dist/esm/enums.js.map +0 -1
  132. package/dist/esm/generated/fields.d.ts.map +0 -1
  133. package/dist/esm/generated/fields.js.map +0 -1
  134. package/dist/esm/index.d.ts.map +0 -1
  135. package/dist/esm/index.js.map +0 -1
  136. package/dist/esm/initiator.d.ts.map +0 -1
  137. package/dist/esm/initiator.js.map +0 -1
  138. package/dist/esm/message.d.ts.map +0 -1
  139. package/dist/esm/message.js.map +0 -1
  140. package/dist/esm/native.d.ts.map +0 -1
  141. package/dist/esm/native.js.map +0 -1
  142. package/dist/esm/session-id.d.ts.map +0 -1
  143. package/dist/esm/session-id.js.map +0 -1
  144. package/dist/esm/session-settings.d.ts.map +0 -1
  145. package/dist/esm/session-settings.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  #include "acceptor_wrap.h"
2
2
 
3
+ #include <memory>
3
4
  #include <string>
4
5
 
5
6
  #include "quickfix/FileLog.h"
@@ -8,7 +9,10 @@
8
9
  #include "engine_common.h"
9
10
  #include "engine_workers.h"
10
11
  #include "errors.h"
12
+ #include "session_id_wrap.h"
13
+ #include "session_op_gate.h"
11
14
  #include "session_settings_wrap.h"
15
+ #include "session_wrap.h"
12
16
 
13
17
  namespace napi_quickfix {
14
18
 
@@ -34,6 +38,8 @@ Napi::Object AcceptorWrap::Init(Napi::Env env, Napi::Object exports) {
34
38
  InstanceMethod("start", &AcceptorWrap::Start),
35
39
  InstanceMethod("stop", &AcceptorWrap::Stop),
36
40
  InstanceMethod("isLoggedOn", &AcceptorWrap::IsLoggedOn),
41
+ InstanceMethod("getSessions", &AcceptorWrap::GetSessions),
42
+ InstanceMethod("getSession", &AcceptorWrap::GetSession),
37
43
  InstanceMethod("ref", &AcceptorWrap::Ref),
38
44
  InstanceMethod("unref", &AcceptorWrap::Unref),
39
45
  });
@@ -76,16 +82,32 @@ AcceptorWrap::AcceptorWrap(const Napi::CallbackInfo& info)
76
82
  storeFactory_ = std::make_unique<FIX::FileStoreFactory>(settings);
77
83
  }
78
84
 
85
+ std::unique_ptr<FIX::LogFactory> logFactory;
79
86
  if (log == "none") {
80
- logFactory_ = std::make_unique<NullLogFactory>();
87
+ logFactory = std::make_unique<NullLogFactory>();
81
88
  } else if (log == "file") {
82
- logFactory_ = std::make_unique<FIX::FileLogFactory>(settings);
89
+ logFactory = std::make_unique<FIX::FileLogFactory>(settings);
83
90
  } else {
84
- logFactory_ = std::make_unique<FIX::ScreenLogFactory>(settings);
91
+ logFactory = std::make_unique<FIX::ScreenLogFactory>(settings);
85
92
  }
93
+ logFactory_ = std::make_unique<TrackingLogFactory>(std::move(logFactory));
86
94
 
87
- acceptor_ = std::make_unique<FIX::SocketAcceptor>(
88
- *bridge_, *storeFactory_, settings, *logFactory_);
95
+ RejectDuplicateSessions(settings, "acceptor");
96
+ try {
97
+ acceptor_ = std::make_unique<SocketAcceptor>(
98
+ *bridge_, *storeFactory_, settings, *logFactory_);
99
+ } catch (...) {
100
+ // A [SESSION] after the first valid one threw: QuickFIX has leaked the
101
+ // sessions it had already created (see DestroyOrphanedSessions) and the
102
+ // engine-level Log it requested before creating any of them (see
103
+ // TrackingLogFactory). Both would otherwise only have been reclaimed by
104
+ // the engine destructor that never runs.
105
+ DestroyOrphanedSessions(settings, "acceptor");
106
+ logFactory_->DestroyOrphanedEngineLog();
107
+ throw;
108
+ }
109
+ sessionIDs_ = acceptor_->getSessions();
110
+ SessionOpGate::Register(sessionIDs_, gate_);
89
111
  } NQ_CATCH(env)
90
112
 
91
113
  // Deterministic teardown on env shutdown, before the TSFN is finalized. See
@@ -104,19 +126,30 @@ AcceptorWrap::~AcceptorWrap() {
104
126
  cleanupHook_.Remove(env_);
105
127
  }
106
128
  Teardown(true);
129
+ DestroyEngine();
107
130
  }
108
131
 
109
132
  void AcceptorWrap::Teardown(bool force) {
110
133
  // Runs on the MAIN thread. Ordering (see InitiatorWrap::Teardown for detail):
111
134
  // 1. Deactivate the bridge so callbacks during stop() pass through without
112
135
  // touching the TSFN or waiting on the frozen JS loop (breaks deadlock).
113
- // 2. stop(force): joins the QuickFIX network threads.
114
- // 3. Abort the TSFN: safe now that no producer thread remains.
115
- if (stopped_) return;
136
+ // 2. Join the stop() worker if one is in flight, so the engine is never
137
+ // destroyed under its FIX::Acceptor::stop() / gate freeze. Hence the
138
+ // early return is keyed on tornDown_, not on stopped_.
139
+ // 3. stop(force): joins the QuickFIX network threads (a no-op if the
140
+ // worker's stop() completed). That join lasts until the network thread's
141
+ // next poll timeout, up to 1 s (see SocketAcceptor in the header).
142
+ // 4. Abort the TSFN: safe now that no producer thread remains.
143
+ if (tornDown_) return;
144
+ tornDown_ = true;
116
145
  stopped_ = true;
117
146
  if (bridge_) {
118
147
  bridge_->Deactivate();
119
148
  }
149
+ if (stopSync_) {
150
+ stopSync_->Wait();
151
+ stopSync_.reset();
152
+ }
120
153
  if (acceptor_ && started_) {
121
154
  try {
122
155
  acceptor_->stop(force);
@@ -128,6 +161,32 @@ void AcceptorWrap::Teardown(bool force) {
128
161
  }
129
162
  }
130
163
 
164
+ // Destroy the FIX engine (and with it every FIX::Session it owns) on the JS
165
+ // thread, with no session operation in flight on a libuv thread. Called from
166
+ // the destructor only; stop() uses DestroyStoppedEngine() below instead, as
167
+ // its worker has already Freeze()d the gate on the worker thread (see Stop()).
168
+ // The gate is per engine, so Freeze() here only waits for operations on THIS
169
+ // engine's sessions, and Teardown() has already deactivated its bridge and
170
+ // joined its network threads: none of those operations can be blocked on the
171
+ // JS thread (or on a session mutex), so Freeze() cannot deadlock. Operations
172
+ // on other engines -- which may well be blocked in a BlockingCall that needs
173
+ // this very thread -- are not waited for.
174
+ void AcceptorWrap::DestroyEngine() {
175
+ if (!acceptor_) return;
176
+ gate_->Freeze();
177
+ SessionOpGate::Unregister(sessionIDs_, gate_.get());
178
+ acceptor_.reset();
179
+ gate_->Unfreeze();
180
+ }
181
+
182
+ // The stop() path: the gate was frozen by the stop worker; destroy and lift
183
+ // the freeze. JS thread only.
184
+ void AcceptorWrap::DestroyStoppedEngine() {
185
+ SessionOpGate::Unregister(sessionIDs_, gate_.get());
186
+ acceptor_.reset();
187
+ gate_->Unfreeze();
188
+ }
189
+
131
190
  Napi::Value AcceptorWrap::Start(const Napi::CallbackInfo& info) {
132
191
  Napi::Env env = info.Env();
133
192
  auto deferred = Napi::Promise::Deferred::New(env);
@@ -153,9 +212,10 @@ Napi::Value AcceptorWrap::Start(const Napi::CallbackInfo& info) {
153
212
  auto* worker = new EngineOpWorker(
154
213
  env, info.This().As<Napi::Object>(),
155
214
  [acceptor]() { acceptor->start(); },
156
- [self](bool ok) {
215
+ [self](bool ok, const std::shared_ptr<EngineOpWorker::Settlement>& s) {
157
216
  self->busy_ = false;
158
217
  if (!ok) self->started_ = false;
218
+ s->Settle();
159
219
  });
160
220
  Napi::Promise promise = worker->Promise();
161
221
  worker->Queue();
@@ -169,6 +229,14 @@ Napi::Value AcceptorWrap::Stop(const Napi::CallbackInfo& info) {
169
229
  force = info[0].ToBoolean().Value();
170
230
  }
171
231
  auto deferred = Napi::Promise::Deferred::New(env);
232
+ // A stop() is in flight (its worker is running, or a graceful stop is
233
+ // waiting for its queued 'logout' events to be dispatched before destroying
234
+ // the engine): join it. Resolving right away here -- `stopped_` is already
235
+ // set -- would break the README's promise that the sessions are gone once
236
+ // stop() settles. The first call's `force` applies to both.
237
+ if (!stopPromise_.IsEmpty()) {
238
+ return stopPromise_.Value();
239
+ }
172
240
  if (stopped_) {
173
241
  deferred.Resolve(env.Undefined());
174
242
  return deferred.Promise();
@@ -179,7 +247,8 @@ Napi::Value AcceptorWrap::Stop(const Napi::CallbackInfo& info) {
179
247
  .Value());
180
248
  return deferred.Promise();
181
249
  }
182
- if (!acceptor_ || !started_) {
250
+ if (!acceptor_) {
251
+ // Already torn down: just release the TSFN and resolve.
183
252
  stopped_ = true;
184
253
  if (bridge_) bridge_->Release();
185
254
  deferred.Resolve(env.Undefined());
@@ -187,38 +256,203 @@ Napi::Value AcceptorWrap::Stop(const Napi::CallbackInfo& info) {
187
256
  }
188
257
  busy_ = true;
189
258
  stopped_ = true;
259
+ // Lets Teardown() wait for the worker's operation instead of destroying the
260
+ // engine under it; cleared in onSettled once the operation has returned.
261
+ auto stopDone = std::make_shared<StopCompletion>();
262
+ stopSync_ = stopDone;
190
263
 
264
+ // A never-started engine takes the same async path (minus the stop() call):
265
+ // its FIX::Session objects are registered from construction, so a session
266
+ // operation may already be in flight against them and the destruction below
267
+ // must wait for it just the same.
191
268
  FIX::SocketAcceptor* acceptor = acceptor_.get();
192
269
  ApplicationBridge* bridge = bridge_.get();
270
+ const bool started = started_;
271
+ std::shared_ptr<SessionOpGate> gate = gate_;
193
272
  auto* self = this;
194
273
  auto* worker = new EngineOpWorker(
195
274
  env, info.This().As<Napi::Object>(),
196
- [acceptor, force]() { acceptor->stop(force); },
197
- [self, bridge, force](bool /*ok*/) {
275
+ // Runs on the worker thread. stop()'s onLogout/toAdmin callbacks fire on
276
+ // QuickFIX threads while the main loop is free to service the TSFN.
277
+ [acceptor, gate, started, force, stopDone]() {
278
+ // Signal completion on every exit of this lambda, success or throw,
279
+ // and only after the gate freeze below has returned as well.
280
+ struct SignalOnExit {
281
+ std::shared_ptr<StopCompletion> done;
282
+ ~SignalOnExit() { done->Signal(); }
283
+ } signalOnExit{stopDone};
284
+ try {
285
+ if (started) acceptor->stop(force);
286
+ } catch (...) {
287
+ gate->Freeze();
288
+ throw;
289
+ }
290
+ // stop() has joined the network threads. Now block new session
291
+ // operations (SendToTargetWorker) on THIS engine's
292
+ // sessions and wait for the in-flight ones to finish: they hold raw
293
+ // FIX::Session pointers that the destruction below is about to
294
+ // invalidate. Waiting HERE, on the worker thread, keeps the JS loop
295
+ // free for any BlockingCall such an operation is still making (e.g.
296
+ // sendToTarget() -> toApp). Other engines' operations are unaffected.
297
+ gate->Freeze();
298
+ },
299
+ [self, bridge, env, force, gate](
300
+ bool ok, const std::shared_ptr<EngineOpWorker::Settlement>& s) {
198
301
  self->busy_ = false;
199
- if (bridge) {
200
- if (force) {
201
- bridge->Abort();
202
- } else {
203
- bridge->Release();
302
+ self->stopSync_.reset(); // the worker's operation has returned
303
+ // Settles the promise and lets a later stop() start over (the
304
+ // in-flight stop is finished, successfully or not).
305
+ auto settle = [self, s]() {
306
+ self->stopPromise_.Reset();
307
+ s->Settle();
308
+ };
309
+ if (!ok) {
310
+ // stop() threw part-way: the network threads may still be running,
311
+ // so the engine is NOT safe to destroy. Keep it, let session
312
+ // operations through again and clear stopped_ so that a later
313
+ // stop() -- or the destructor's Teardown() -- retries. Mirrors
314
+ // Start() clearing started_ on failure. The bridge is released (or
315
+ // aborted) as on success, so a failed stop() does not keep the
316
+ // event loop alive: the TSFN holds a loop reference until it is
317
+ // released, and a script that catches the rejection and falls off
318
+ // the end would otherwise never exit. From here on the engine's
319
+ // callbacks pass through without reaching JS; the retried stop()
320
+ // then destroys the engine as soon as its worker settles, since
321
+ // RunAfterQueued() refuses a released bridge.
322
+ self->stopped_ = false;
323
+ self->gate_->Unfreeze();
324
+ if (bridge) {
325
+ if (force) {
326
+ bridge->Abort();
327
+ } else {
328
+ bridge->Release();
329
+ }
204
330
  }
331
+ settle();
332
+ return;
333
+ }
334
+ // stop() has completed: the network threads are joined and session
335
+ // operations are frozen. Destroy the engine on the JS thread rather
336
+ // than at GC: QuickFIX only deletes its FIX::Session objects -- and
337
+ // removes them from the process-wide registry that lookupSession() /
338
+ // sendToTarget() read -- in the engine destructor. Deferring that to GC
339
+ // would leave stale Session handles resolvable and make a new engine
340
+ // with the same SessionIDs fail with ConfigError "Duplicate Session"
341
+ // until the old wrap happened to be collected. A stopped engine cannot
342
+ // be restarted anyway (start() rejects once stopped_ is set).
343
+ if (force || bridge == nullptr) {
344
+ // Force: abort the TSFN, discarding any callback still queued (a
345
+ // 'logout' listener may therefore never run), and destroy right away.
346
+ if (bridge) bridge->Abort();
347
+ self->DestroyStoppedEngine();
348
+ settle();
349
+ return;
350
+ }
351
+ // Graceful: the 'logout' events stop() fired are still queued in the
352
+ // bridge, and their listeners may read the session they name
353
+ // (getSession() / Session handles are documented to stay live until
354
+ // stop() settles). Nothing orders the TSFN's dispatch of those events
355
+ // before this AsyncWorker completion, so destroy the engine -- and
356
+ // settle the promise -- only once they have been dispatched: queue the
357
+ // destruction through the bridge's own FIFO behind them, then release
358
+ // the TSFN so it closes once drained. The owner reference held by the
359
+ // settlement keeps this wrap alive until then.
360
+ const bool queued =
361
+ bridge->RunAfterQueued(env, [self, s, settle, gate](Napi::Env e) {
362
+ if (e == nullptr) {
363
+ // Discarded: the environment is being torn down. The wrap's
364
+ // own teardown (cleanup hook, then destructor) destroys the
365
+ // engine; touch nothing but the settlement and the gate,
366
+ // which this closure owns (the gate by shared_ptr). In
367
+ // particular do not reach through `self`: what keeps the
368
+ // wrap alive until here is the TSFN's reference on the
369
+ // napi_env (the env -- and with it every ObjectWrap
370
+ // finalizer -- is deleted only after the TSFN has dropped
371
+ // its queue), a Node internal this code must not lean on.
372
+ // The promise reference needs no special care: the wrap
373
+ // destructor deletes it, and runs with the env valid.
374
+ //
375
+ // The freeze the stop worker took is still held: lift it,
376
+ // as DestroyStoppedEngine() would have. A SendToTargetWorker
377
+ // parked in OpScope since then is otherwise never woken, and
378
+ // the destructor's own DestroyEngine() -- which nests its
379
+ // freeze inside this one -- never brings the count to zero
380
+ // either; the libuv pool thread it sits on would be parked
381
+ // for good, and whoever drains the pool at exit (a Worker's
382
+ // terminate(), the process teardown) would hang on it. Once
383
+ // woken it runs against the engine, still intact at this
384
+ // point (the bridge is inactive, so a callback it makes
385
+ // passes through), or is refused by Acquire() if the engine
386
+ // has already been unregistered; the destructor's Freeze()
387
+ // waits for it either way.
388
+ s->Abandon();
389
+ gate->Unfreeze();
390
+ return;
391
+ }
392
+ self->DestroyStoppedEngine();
393
+ settle();
394
+ });
395
+ bridge->Release();
396
+ if (!queued) {
397
+ self->DestroyStoppedEngine();
398
+ settle();
205
399
  }
206
400
  });
207
401
  Napi::Promise promise = worker->Promise();
402
+ stopPromise_ = Napi::Persistent(static_cast<Napi::Object>(promise));
208
403
  worker->Queue();
209
404
  return promise;
210
405
  }
211
406
 
407
+ // isLoggedOn(sessionID?): with no argument, true if ANY session of this engine
408
+ // is logged on (FIX::Initiator/Acceptor::isLoggedOn); with a SessionID, true
409
+ // only if THAT session belongs to this engine and is logged on. Always false
410
+ // before start() and once stop() has settled; while a graceful stop() is in
411
+ // progress it still reports the live state (the engine is only destroyed when
412
+ // the stop() promise settles).
212
413
  Napi::Value AcceptorWrap::IsLoggedOn(const Napi::CallbackInfo& info) {
213
414
  Napi::Env env = info.Env();
214
- if (!acceptor_ || !started_ || stopped_) {
415
+ const bool hasId = info.Length() >= 1 && !info[0].IsUndefined();
416
+ // Validate the argument even when the engine is down, for a stable API.
417
+ SessionIDWrap* id =
418
+ hasId ? SessionIDWrap::UnwrapArg(env, info[0], "sessionID") : nullptr;
419
+ if (!acceptor_ || !started_) {
215
420
  return Napi::Boolean::New(env, false);
216
421
  }
217
422
  NQ_TRY(env) {
218
- return Napi::Boolean::New(env, acceptor_->isLoggedOn());
423
+ if (id == nullptr) {
424
+ return Napi::Boolean::New(env, acceptor_->isLoggedOn());
425
+ }
426
+ FIX::Session* session = acceptor_->getSession(id->SessionID());
427
+ return Napi::Boolean::New(env,
428
+ session != nullptr && session->isLoggedOn());
219
429
  } NQ_CATCH(env)
220
430
  }
221
431
 
432
+ // getSessions(): SessionID[] -- the sessions this engine was configured with.
433
+ Napi::Value AcceptorWrap::GetSessions(const Napi::CallbackInfo& info) {
434
+ Napi::Env env = info.Env();
435
+ Napi::Array out = Napi::Array::New(env, sessionIDs_.size());
436
+ uint32_t i = 0;
437
+ for (const FIX::SessionID& sid : sessionIDs_) {
438
+ out.Set(i++, SessionIDWrap::NewInstance(env, sid));
439
+ }
440
+ return out;
441
+ }
442
+
443
+ // getSession(sessionID): Session | undefined -- undefined if the id is not one
444
+ // of this engine's sessions or stop() has settled (its sessions are destroyed
445
+ // then, see Stop()). While stop() is still in progress the sessions are alive
446
+ // and a handle is returned, e.g. for a 'logout' listener reading final seq nums.
447
+ Napi::Value AcceptorWrap::GetSession(const Napi::CallbackInfo& info) {
448
+ Napi::Env env = info.Env();
449
+ SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[0], "sessionID");
450
+ if (!acceptor_ || !acceptor_->has(id->SessionID())) {
451
+ return env.Undefined();
452
+ }
453
+ return SessionWrap::NewInstance(env, id->SessionID(), gate_);
454
+ }
455
+
222
456
  Napi::Value AcceptorWrap::Ref(const Napi::CallbackInfo& info) {
223
457
  Napi::Env env = info.Env();
224
458
  if (bridge_) bridge_->Ref(env);
@@ -4,15 +4,45 @@
4
4
  #include <napi.h>
5
5
 
6
6
  #include <memory>
7
+ #include <set>
7
8
 
8
9
  #include "quickfix/Log.h"
9
10
  #include "quickfix/MessageStore.h"
11
+ #include "quickfix/SessionID.h"
10
12
  #include "quickfix/SocketAcceptor.h"
11
13
 
12
14
  #include "application_bridge.h"
15
+ #include "engine_common.h"
16
+ #include "engine_workers.h"
17
+ #include "session_op_gate.h"
13
18
 
14
19
  namespace napi_quickfix {
15
20
 
21
+ // FIX::SocketAcceptor whose stop() leaves the SocketServer to the acceptor's
22
+ // own network thread. QuickFIX's onStop() runs on the STOPPING thread (the stop
23
+ // worker, or the JS thread in Teardown()) right after FIX::Acceptor::stop()
24
+ // sets the stop flag, and calls m_pServer->close(). That close shuts the
25
+ // listening socket down, which wakes the network thread's poll(), and the
26
+ // network thread -- now seeing the stop flag -- leaves its loop, closes the
27
+ // server again and DELETES it. Nothing orders the two: a stopping thread
28
+ // preempted inside close() resumes on the freed SocketServer and walks its
29
+ // freed socket map (SIGSEGV in std::_Rb_tree_increment, issue #24). Even when
30
+ // it wins, the two close() calls close the same descriptor numbers twice, and
31
+ // the second may hit a descriptor the process has reopened in between.
32
+ //
33
+ // With onStop() a no-op -- as FIX::SocketInitiator's already is -- only the
34
+ // network thread touches the server: it sees the stop flag once its poll()
35
+ // times out (SocketServer's 1 s timeout), and stop() waits for that in its
36
+ // thread join. onStop() is private in FIX::SocketAcceptor but virtual (pure in
37
+ // FIX::Acceptor), so it can be overridden here.
38
+ class SocketAcceptor final : public FIX::SocketAcceptor {
39
+ public:
40
+ using FIX::SocketAcceptor::SocketAcceptor;
41
+
42
+ private:
43
+ void onStop() override {}
44
+ };
45
+
16
46
  // Napi::ObjectWrap over FIX::SocketAcceptor. Mirrors InitiatorWrap.
17
47
  //
18
48
  // new Acceptor({ settings, handlers?, store?, log? })
@@ -28,20 +58,49 @@ class AcceptorWrap : public Napi::ObjectWrap<AcceptorWrap> {
28
58
  Napi::Value Start(const Napi::CallbackInfo& info);
29
59
  Napi::Value Stop(const Napi::CallbackInfo& info);
30
60
  Napi::Value IsLoggedOn(const Napi::CallbackInfo& info);
61
+ Napi::Value GetSessions(const Napi::CallbackInfo& info);
62
+ Napi::Value GetSession(const Napi::CallbackInfo& info);
31
63
  Napi::Value Ref(const Napi::CallbackInfo& info);
32
64
  Napi::Value Unref(const Napi::CallbackInfo& info);
33
65
 
34
66
  void Teardown(bool force);
67
+ // Destroy the FIX engine under gate_->Freeze(); destructor only.
68
+ void DestroyEngine();
69
+ // Destroy the FIX engine once stop() has frozen the gate; Stop() only.
70
+ void DestroyStoppedEngine();
35
71
 
36
72
  // Destruct order: acceptor first (declared last), then log, store, bridge.
37
73
  std::unique_ptr<ApplicationBridge> bridge_;
38
74
  std::unique_ptr<FIX::MessageStoreFactory> storeFactory_;
39
- std::unique_ptr<FIX::LogFactory> logFactory_;
75
+ // Wraps the real factory so a failed engine constructor can reclaim the
76
+ // engine-level Log QuickFIX would otherwise leak (see engine_common.h).
77
+ std::unique_ptr<TrackingLogFactory> logFactory_;
78
+ // Gate between this engine's destruction and the libuv-thread session
79
+ // operations on ITS sessions (see session_op_gate.h). Shared with the stop
80
+ // worker; registered for sessionIDs_ at construction, unregistered when the
81
+ // engine is destroyed.
82
+ std::shared_ptr<SessionOpGate> gate_ = std::make_shared<SessionOpGate>();
40
83
  std::unique_ptr<FIX::SocketAcceptor> acceptor_;
41
84
 
85
+ // The SessionIDs this engine was configured with, copied at construction so
86
+ // getSessions() still answers after stop() has destroyed the engine.
87
+ std::set<FIX::SessionID> sessionIDs_;
88
+
42
89
  bool started_ = false;
90
+ // Set by stop() when it launches its worker (so start() rejects from then
91
+ // on) and by Teardown(); cleared again if that stop() failed. It is NOT
92
+ // "the engine is stopped": a stop() may be in flight.
43
93
  bool stopped_ = false;
94
+ bool tornDown_ = false; // Teardown() has run (it runs once)
44
95
  bool busy_ = false; // guards against overlapping start/stop AsyncWorkers
96
+ // Signalled by the in-flight stop() worker once its operation has returned;
97
+ // non-null from Stop() launching the worker until that stop settles on the
98
+ // JS thread. Teardown() waits on it (see there).
99
+ std::shared_ptr<StopCompletion> stopSync_;
100
+ // The promise of the stop() in flight, from the call until it settles
101
+ // (which, for a graceful stop, is after its worker's OnOK). A stop() made
102
+ // meanwhile returns this same promise instead of resolving early.
103
+ Napi::ObjectReference stopPromise_;
45
104
 
46
105
  static void CleanupEntry(AcceptorWrap* self);
47
106
 
package/cpp/addon.cpp CHANGED
@@ -1,5 +1,7 @@
1
- // Module init for @homaiohq/napi-quickfix: registers all wrappers, enums, and
2
- // module-level functions.
1
+ // Module init for @homaiohq/napi-quickfix: registers all wrappers and
2
+ // module-level functions. FIX constants (field tags, value groups) are not
3
+ // exported here: they are generated on the TS side from the QuickFIX headers
4
+ // (scripts/gen-fields.mjs, scripts/gen-values.mjs).
3
5
 
4
6
  #include <napi.h>
5
7
 
@@ -13,21 +15,22 @@
13
15
 
14
16
  #include "acceptor_wrap.h"
15
17
  #include "data_dictionary_wrap.h"
18
+ #include "group_wrap.h"
16
19
  #include "initiator_wrap.h"
17
20
  #include "message_wrap.h"
18
21
  #include "session_id_wrap.h"
19
22
  #include "session_settings_wrap.h"
23
+ #include "session_wrap.h"
20
24
 
21
25
  namespace napi_quickfix {
22
26
 
23
- // Defined in session_static.cpp / enums.cpp.
27
+ // Defined in session_static.cpp.
24
28
  void RegisterSessionStatic(Napi::Env env, Napi::Object exports);
25
- void RegisterEnums(Napi::Env env, Napi::Object exports);
26
29
 
27
30
  namespace {
28
31
 
29
32
  // Keep in sync with package.json "version".
30
- constexpr const char* kAddonVersion = "0.1.0";
33
+ constexpr const char* kAddonVersion = "0.3.0";
31
34
 
32
35
  Napi::Value Version(const Napi::CallbackInfo& info) {
33
36
  return Napi::String::New(info.Env(), kAddonVersion);
@@ -60,14 +63,15 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
60
63
  Napi::Function::New(env, QuickfixParseMsgType, "quickfixParseMsgType"));
61
64
 
62
65
  MessageWrap::Init(env, exports);
66
+ GroupWrap::Init(env, exports);
63
67
  SessionIDWrap::Init(env, exports);
64
68
  SessionSettingsWrap::Init(env, exports);
65
69
  DataDictionaryWrap::Init(env, exports);
70
+ SessionWrap::Init(env, exports);
66
71
  InitiatorWrap::Init(env, exports);
67
72
  AcceptorWrap::Init(env, exports);
68
73
 
69
74
  RegisterSessionStatic(env, exports);
70
- RegisterEnums(env, exports);
71
75
 
72
76
  return exports;
73
77
  }