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