@homaiohq/napi-quickfix 0.0.0-stage → 0.1.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 (131) hide show
  1. package/CMakeLists.txt +131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +421 -2
  4. package/THIRD_PARTY_LICENSES +48 -0
  5. package/cpp/acceptor_wrap.cpp +234 -0
  6. package/cpp/acceptor_wrap.h +57 -0
  7. package/cpp/addon.cpp +83 -0
  8. package/cpp/application_bridge.cpp +382 -0
  9. package/cpp/application_bridge.h +151 -0
  10. package/cpp/data_dictionary_wrap.cpp +79 -0
  11. package/cpp/data_dictionary_wrap.h +37 -0
  12. package/cpp/engine_common.cpp +5 -0
  13. package/cpp/engine_common.h +19 -0
  14. package/cpp/engine_workers.h +86 -0
  15. package/cpp/enums.cpp +66 -0
  16. package/cpp/errors.h +142 -0
  17. package/cpp/initiator_wrap.cpp +255 -0
  18. package/cpp/initiator_wrap.h +69 -0
  19. package/cpp/message_wrap.cpp +195 -0
  20. package/cpp/message_wrap.h +53 -0
  21. package/cpp/session_id_wrap.cpp +91 -0
  22. package/cpp/session_id_wrap.h +46 -0
  23. package/cpp/session_settings_wrap.cpp +98 -0
  24. package/cpp/session_settings_wrap.h +40 -0
  25. package/cpp/session_static.cpp +93 -0
  26. package/dist/cjs/acceptor.d.ts +30 -0
  27. package/dist/cjs/acceptor.d.ts.map +1 -0
  28. package/dist/cjs/acceptor.js +39 -0
  29. package/dist/cjs/acceptor.js.map +1 -0
  30. package/dist/cjs/application.d.ts +116 -0
  31. package/dist/cjs/application.d.ts.map +1 -0
  32. package/dist/cjs/application.js +63 -0
  33. package/dist/cjs/application.js.map +1 -0
  34. package/dist/cjs/data-dictionary.d.ts +49 -0
  35. package/dist/cjs/data-dictionary.d.ts.map +1 -0
  36. package/dist/cjs/data-dictionary.js +77 -0
  37. package/dist/cjs/data-dictionary.js.map +1 -0
  38. package/dist/cjs/engine.d.ts +114 -0
  39. package/dist/cjs/engine.d.ts.map +1 -0
  40. package/dist/cjs/engine.js +138 -0
  41. package/dist/cjs/engine.js.map +1 -0
  42. package/dist/cjs/enums.d.ts +22 -0
  43. package/dist/cjs/enums.d.ts.map +1 -0
  44. package/dist/cjs/enums.js +49 -0
  45. package/dist/cjs/enums.js.map +1 -0
  46. package/dist/cjs/generated/fields.d.ts +6123 -0
  47. package/dist/cjs/generated/fields.d.ts.map +1 -0
  48. package/dist/cjs/generated/fields.js +6128 -0
  49. package/dist/cjs/generated/fields.js.map +1 -0
  50. package/dist/cjs/index.d.ts +47 -0
  51. package/dist/cjs/index.d.ts.map +1 -0
  52. package/dist/cjs/index.js +73 -0
  53. package/dist/cjs/index.js.map +1 -0
  54. package/dist/cjs/initiator.d.ts +30 -0
  55. package/dist/cjs/initiator.d.ts.map +1 -0
  56. package/dist/cjs/initiator.js +39 -0
  57. package/dist/cjs/initiator.js.map +1 -0
  58. package/dist/cjs/load-native.cjs +5 -0
  59. package/dist/cjs/message.d.ts +114 -0
  60. package/dist/cjs/message.d.ts.map +1 -0
  61. package/dist/cjs/message.js +162 -0
  62. package/dist/cjs/message.js.map +1 -0
  63. package/dist/cjs/native.d.ts +163 -0
  64. package/dist/cjs/native.d.ts.map +1 -0
  65. package/dist/cjs/native.js +23 -0
  66. package/dist/cjs/native.js.map +1 -0
  67. package/dist/cjs/package.json +1 -0
  68. package/dist/cjs/session-id.d.ts +40 -0
  69. package/dist/cjs/session-id.d.ts.map +1 -0
  70. package/dist/cjs/session-id.js +67 -0
  71. package/dist/cjs/session-id.js.map +1 -0
  72. package/dist/cjs/session-settings.d.ts +47 -0
  73. package/dist/cjs/session-settings.d.ts.map +1 -0
  74. package/dist/cjs/session-settings.js +76 -0
  75. package/dist/cjs/session-settings.js.map +1 -0
  76. package/dist/esm/acceptor.d.ts +30 -0
  77. package/dist/esm/acceptor.d.ts.map +1 -0
  78. package/dist/esm/acceptor.js +35 -0
  79. package/dist/esm/acceptor.js.map +1 -0
  80. package/dist/esm/application.d.ts +116 -0
  81. package/dist/esm/application.d.ts.map +1 -0
  82. package/dist/esm/application.js +58 -0
  83. package/dist/esm/application.js.map +1 -0
  84. package/dist/esm/data-dictionary.d.ts +49 -0
  85. package/dist/esm/data-dictionary.d.ts.map +1 -0
  86. package/dist/esm/data-dictionary.js +73 -0
  87. package/dist/esm/data-dictionary.js.map +1 -0
  88. package/dist/esm/engine.d.ts +114 -0
  89. package/dist/esm/engine.d.ts.map +1 -0
  90. package/dist/esm/engine.js +134 -0
  91. package/dist/esm/engine.js.map +1 -0
  92. package/dist/esm/enums.d.ts +22 -0
  93. package/dist/esm/enums.d.ts.map +1 -0
  94. package/dist/esm/enums.js +45 -0
  95. package/dist/esm/enums.js.map +1 -0
  96. package/dist/esm/generated/fields.d.ts +6123 -0
  97. package/dist/esm/generated/fields.d.ts.map +1 -0
  98. package/dist/esm/generated/fields.js +6125 -0
  99. package/dist/esm/generated/fields.js.map +1 -0
  100. package/dist/esm/index.d.ts +47 -0
  101. package/dist/esm/index.d.ts.map +1 -0
  102. package/dist/esm/index.js +52 -0
  103. package/dist/esm/index.js.map +1 -0
  104. package/dist/esm/initiator.d.ts +30 -0
  105. package/dist/esm/initiator.d.ts.map +1 -0
  106. package/dist/esm/initiator.js +35 -0
  107. package/dist/esm/initiator.js.map +1 -0
  108. package/dist/esm/load-native.cjs +5 -0
  109. package/dist/esm/message.d.ts +114 -0
  110. package/dist/esm/message.d.ts.map +1 -0
  111. package/dist/esm/message.js +156 -0
  112. package/dist/esm/message.js.map +1 -0
  113. package/dist/esm/native.d.ts +163 -0
  114. package/dist/esm/native.d.ts.map +1 -0
  115. package/dist/esm/native.js +17 -0
  116. package/dist/esm/native.js.map +1 -0
  117. package/dist/esm/session-id.d.ts +40 -0
  118. package/dist/esm/session-id.d.ts.map +1 -0
  119. package/dist/esm/session-id.js +63 -0
  120. package/dist/esm/session-id.js.map +1 -0
  121. package/dist/esm/session-settings.d.ts +47 -0
  122. package/dist/esm/session-settings.d.ts.map +1 -0
  123. package/dist/esm/session-settings.js +72 -0
  124. package/dist/esm/session-settings.js.map +1 -0
  125. package/package.json +83 -4
  126. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  127. package/prebuilds/darwin-x64/node.napi.node +0 -0
  128. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  129. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  130. package/prebuilds/win32-x64/node.napi.node +0 -0
  131. package/src/load-native.cjs +5 -0
@@ -0,0 +1,86 @@
1
+ #ifndef NAPI_QUICKFIX_ENGINE_WORKERS_H
2
+ #define NAPI_QUICKFIX_ENGINE_WORKERS_H
3
+
4
+ #include <napi.h>
5
+
6
+ #include <functional>
7
+ #include <utility>
8
+
9
+ #include "errors.h"
10
+
11
+ namespace napi_quickfix {
12
+
13
+ // A generic AsyncWorker that runs a blocking QuickFIX engine operation
14
+ // (start/stop) on a libuv worker thread and resolves/rejects a JS Promise.
15
+ //
16
+ // Why off-thread: start()/stop() synchronously invoke FIX::Application callbacks
17
+ // (toAdmin/toApp during send-on-logon, onLogout during stop) which the
18
+ // ApplicationBridge dispatches back to the JS thread via a TSFN BlockingCall +
19
+ // future. If the operation ran ON the JS thread, the event loop couldn't service
20
+ // the TSFN trampoline and we'd deadlock. Running it on a worker thread keeps the
21
+ // main event loop free to run CallJs.
22
+ //
23
+ // Lifetime: the worker holds a Napi::ObjectReference to the owning wrap object so
24
+ // it (and its bridge / engine / factories) can't be GC'd while the op is in
25
+ // flight. The ref is released in OnOK/OnError (both run on the JS thread).
26
+ class EngineOpWorker : public Napi::AsyncWorker {
27
+ public:
28
+ // onSettled receives `true` if the op succeeded, `false` if it threw.
29
+ EngineOpWorker(Napi::Env env, Napi::Object owner, std::function<void()> op,
30
+ std::function<void(bool)> onSettled)
31
+ : Napi::AsyncWorker(env),
32
+ deferred_(Napi::Promise::Deferred::New(env)),
33
+ op_(std::move(op)),
34
+ onSettled_(std::move(onSettled)) {
35
+ ownerRef_ = Napi::Persistent(owner);
36
+ }
37
+
38
+ Napi::Promise Promise() { return deferred_.Promise(); }
39
+
40
+ // Runs on a libuv worker thread. NO Napi/JS access allowed here.
41
+ void Execute() override {
42
+ try {
43
+ op_();
44
+ } catch (const std::exception& e) {
45
+ err_.Capture(e);
46
+ } catch (...) {
47
+ err_.has = true;
48
+ err_.message = "unknown native error";
49
+ err_.fixErrorName = "Error";
50
+ }
51
+ }
52
+
53
+ // Runs on the JS thread.
54
+ void OnOK() override {
55
+ Napi::Env env = Env();
56
+ Napi::HandleScope scope(env);
57
+ if (onSettled_) onSettled_(!err_.has);
58
+ if (err_.has) {
59
+ deferred_.Reject(err_.ToError(env).Value());
60
+ } else {
61
+ deferred_.Resolve(env.Undefined());
62
+ }
63
+ ownerRef_.Reset();
64
+ }
65
+
66
+ // Runs on the JS thread (only if AsyncWorker::SetError was used; we don't, so
67
+ // errors flow through OnOK — but provide OnError for completeness/safety).
68
+ void OnError(const Napi::Error& e) override {
69
+ Napi::Env env = Env();
70
+ Napi::HandleScope scope(env);
71
+ if (onSettled_) onSettled_(false);
72
+ deferred_.Reject(e.Value());
73
+ ownerRef_.Reset();
74
+ }
75
+
76
+ private:
77
+ Napi::Promise::Deferred deferred_;
78
+ Napi::ObjectReference ownerRef_;
79
+ std::function<void()> op_;
80
+ std::function<void(bool)> onSettled_;
81
+ CapturedError err_;
82
+ };
83
+
84
+ } // namespace napi_quickfix
85
+
86
+ #endif // NAPI_QUICKFIX_ENGINE_WORKERS_H
package/cpp/enums.cpp ADDED
@@ -0,0 +1,66 @@
1
+ #include <napi.h>
2
+
3
+ namespace napi_quickfix {
4
+
5
+ namespace {
6
+
7
+ void SetInt(Napi::Object obj, const char* key, int value) {
8
+ obj.Set(key, Napi::Number::New(obj.Env(), value));
9
+ }
10
+
11
+ void SetStr(Napi::Object obj, const char* key, const char* value) {
12
+ obj.Set(key, Napi::String::New(obj.Env(), value));
13
+ }
14
+
15
+ } // namespace
16
+
17
+ // Exposes commonly-needed FIX enum values (MsgType, Side, ...) as `exports.enums`.
18
+ // Field tags are NOT here: `FIELD` is generated on the TS side from QuickFIX's
19
+ // FixFieldNumbers.h (scripts/gen-fields.mjs) so it covers every field.
20
+ // Not exhaustive by design — a curated, useful subset.
21
+ void RegisterEnums(Napi::Env env, Napi::Object exports) {
22
+ Napi::Object enums = Napi::Object::New(env);
23
+
24
+ // --- MsgType ------------------------------------------------------------
25
+ Napi::Object msgType = Napi::Object::New(env);
26
+ SetStr(msgType, "Heartbeat", "0");
27
+ SetStr(msgType, "TestRequest", "1");
28
+ SetStr(msgType, "ResendRequest", "2");
29
+ SetStr(msgType, "Reject", "3");
30
+ SetStr(msgType, "SequenceReset", "4");
31
+ SetStr(msgType, "Logout", "5");
32
+ SetStr(msgType, "Logon", "A");
33
+ SetStr(msgType, "NewOrderSingle", "D");
34
+ SetStr(msgType, "ExecutionReport", "8");
35
+ SetStr(msgType, "OrderCancelRequest", "F");
36
+ SetStr(msgType, "OrderCancelReplaceRequest", "G");
37
+ SetStr(msgType, "OrderCancelReject", "9");
38
+ enums.Set("MsgType", msgType);
39
+
40
+ // --- Side ---------------------------------------------------------------
41
+ Napi::Object side = Napi::Object::New(env);
42
+ SetStr(side, "Buy", "1");
43
+ SetStr(side, "Sell", "2");
44
+ SetStr(side, "SellShort", "5");
45
+ enums.Set("Side", side);
46
+
47
+ // --- OrdType ------------------------------------------------------------
48
+ Napi::Object ordType = Napi::Object::New(env);
49
+ SetStr(ordType, "Market", "1");
50
+ SetStr(ordType, "Limit", "2");
51
+ SetStr(ordType, "Stop", "3");
52
+ SetStr(ordType, "StopLimit", "4");
53
+ enums.Set("OrdType", ordType);
54
+
55
+ // --- TimeInForce --------------------------------------------------------
56
+ Napi::Object tif = Napi::Object::New(env);
57
+ SetStr(tif, "Day", "0");
58
+ SetStr(tif, "GoodTillCancel", "1");
59
+ SetStr(tif, "ImmediateOrCancel", "3");
60
+ SetStr(tif, "FillOrKill", "4");
61
+ enums.Set("TimeInForce", tif);
62
+
63
+ exports.Set("enums", enums);
64
+ }
65
+
66
+ } // namespace napi_quickfix
package/cpp/errors.h ADDED
@@ -0,0 +1,142 @@
1
+ // C++ -> Napi::Error translation for the QuickFIX wrapper.
2
+ //
3
+ // Every wrapper method funnels caught exceptions through here so that JS sees a
4
+ // consistent Error shape:
5
+ // - name: "QuickFixError"
6
+ // - message: the exception's what()
7
+ // - fixError: a discriminator string. For FIX::Exception this is its `.type`
8
+ // (e.g. "Field not found: 35", "Configuration failed", ...) which
9
+ // is stable per concrete type; we ALSO set fixErrorName to the C++
10
+ // class name (e.g. "FieldNotFound") which is the value TS keys on.
11
+ // - detail: the exception's `.detail` when available.
12
+ //
13
+ // NAPI_CPP_EXCEPTIONS is enabled, so throwing a Napi::Error unwinds into a JS
14
+ // throw at the call boundary.
15
+ #ifndef NAPI_QUICKFIX_ERRORS_H
16
+ #define NAPI_QUICKFIX_ERRORS_H
17
+
18
+ #include <napi.h>
19
+
20
+ #include <exception>
21
+ #include <string>
22
+
23
+ #include "quickfix/Exceptions.h"
24
+
25
+ namespace napi_quickfix {
26
+
27
+ // Map a concrete FIX::Exception to a stable class-name discriminator. QuickFIX
28
+ // exceptions don't carry RTTI-friendly names, so we dynamic_cast the well-known
29
+ // concrete types (order: most-specific first is not required since they don't
30
+ // inherit from each other beyond Exception).
31
+ inline const char* FixErrorName(const FIX::Exception& e) {
32
+ if (dynamic_cast<const FIX::ConfigError*>(&e)) return "ConfigError";
33
+ if (dynamic_cast<const FIX::RuntimeError*>(&e)) return "RuntimeError";
34
+ if (dynamic_cast<const FIX::FieldNotFound*>(&e)) return "FieldNotFound";
35
+ if (dynamic_cast<const FIX::FieldConvertError*>(&e)) return "FieldConvertError";
36
+ if (dynamic_cast<const FIX::MessageParseError*>(&e)) return "MessageParseError";
37
+ if (dynamic_cast<const FIX::InvalidMessage*>(&e)) return "InvalidMessage";
38
+ if (dynamic_cast<const FIX::IncorrectDataFormat*>(&e)) return "IncorrectDataFormat";
39
+ if (dynamic_cast<const FIX::IncorrectTagValue*>(&e)) return "IncorrectTagValue";
40
+ if (dynamic_cast<const FIX::IncorrectMessageStructure*>(&e)) return "IncorrectMessageStructure";
41
+ if (dynamic_cast<const FIX::InvalidTagNumber*>(&e)) return "InvalidTagNumber";
42
+ if (dynamic_cast<const FIX::RequiredTagMissing*>(&e)) return "RequiredTagMissing";
43
+ if (dynamic_cast<const FIX::TagNotDefinedForMessage*>(&e)) return "TagNotDefinedForMessage";
44
+ if (dynamic_cast<const FIX::NoTagValue*>(&e)) return "NoTagValue";
45
+ if (dynamic_cast<const FIX::DuplicateFieldNumber*>(&e)) return "DuplicateFieldNumber";
46
+ if (dynamic_cast<const FIX::InvalidMessageType*>(&e)) return "InvalidMessageType";
47
+ if (dynamic_cast<const FIX::UnsupportedMessageType*>(&e)) return "UnsupportedMessageType";
48
+ if (dynamic_cast<const FIX::UnsupportedVersion*>(&e)) return "UnsupportedVersion";
49
+ if (dynamic_cast<const FIX::TagOutOfOrder*>(&e)) return "TagOutOfOrder";
50
+ if (dynamic_cast<const FIX::RepeatedTag*>(&e)) return "RepeatedTag";
51
+ if (dynamic_cast<const FIX::RepeatingGroupCountMismatch*>(&e)) return "RepeatingGroupCountMismatch";
52
+ if (dynamic_cast<const FIX::DoNotSend*>(&e)) return "DoNotSend";
53
+ if (dynamic_cast<const FIX::RejectLogon*>(&e)) return "RejectLogon";
54
+ if (dynamic_cast<const FIX::SessionNotFound*>(&e)) return "SessionNotFound";
55
+ if (dynamic_cast<const FIX::DataDictionaryNotFound*>(&e)) return "DataDictionaryNotFound";
56
+ if (dynamic_cast<const FIX::IOException*>(&e)) return "IOException";
57
+ if (dynamic_cast<const FIX::SocketException*>(&e)) return "SocketException";
58
+ return "FixException";
59
+ }
60
+
61
+ // Build (but do not throw) a Napi::Error from a FIX::Exception.
62
+ inline Napi::Error MakeFixError(Napi::Env env, const FIX::Exception& e) {
63
+ Napi::Error err = Napi::Error::New(env, e.what());
64
+ err.Set("name", Napi::String::New(env, "QuickFixError"));
65
+ err.Set("fixError", Napi::String::New(env, e.type));
66
+ err.Set("fixErrorName", Napi::String::New(env, FixErrorName(e)));
67
+ err.Set("detail", Napi::String::New(env, e.detail));
68
+ return err;
69
+ }
70
+
71
+ // Build (but do not throw) a Napi::Error from a generic std::exception.
72
+ inline Napi::Error MakeStdError(Napi::Env env, const std::exception& e) {
73
+ Napi::Error err = Napi::Error::New(env, e.what());
74
+ err.Set("name", Napi::String::New(env, "QuickFixError"));
75
+ err.Set("fixErrorName", Napi::String::New(env, "Error"));
76
+ return err;
77
+ }
78
+
79
+ // A plain-data capture of an exception's mapped fields, safe to carry across
80
+ // threads (an AsyncWorker's Execute() runs off the JS thread and MUST NOT touch
81
+ // Napi/JS). Populate it in a catch block on the worker thread, then rebuild a
82
+ // Napi::Error from it in OnError/OnOK (which run on the JS thread).
83
+ struct CapturedError {
84
+ bool has = false;
85
+ bool isFix = false;
86
+ std::string message; // what()
87
+ std::string fixError; // FIX::Exception::type (for FIX exceptions)
88
+ std::string fixErrorName; // stable class-name discriminator
89
+ std::string detail; // FIX::Exception::detail
90
+
91
+ void Capture(const std::exception& e) {
92
+ has = true;
93
+ message = e.what();
94
+ if (const auto* fe = dynamic_cast<const FIX::Exception*>(&e)) {
95
+ isFix = true;
96
+ fixError = fe->type;
97
+ fixErrorName = FixErrorName(*fe);
98
+ detail = fe->detail;
99
+ } else {
100
+ isFix = false;
101
+ fixErrorName = "Error";
102
+ }
103
+ }
104
+
105
+ // Build (do not throw) a Napi::Error from the captured fields.
106
+ Napi::Error ToError(Napi::Env env) const {
107
+ Napi::Error err = Napi::Error::New(env, message);
108
+ err.Set("name", Napi::String::New(env, "QuickFixError"));
109
+ if (isFix) {
110
+ err.Set("fixError", Napi::String::New(env, fixError));
111
+ err.Set("fixErrorName", Napi::String::New(env, fixErrorName));
112
+ err.Set("detail", Napi::String::New(env, detail));
113
+ } else {
114
+ err.Set("fixErrorName", Napi::String::New(env, fixErrorName));
115
+ }
116
+ return err;
117
+ }
118
+ };
119
+
120
+ // Translate a caught exception and throw the corresponding Napi::Error. Callers
121
+ // use this inside a catch block, e.g.:
122
+ // try { ... } catch (const std::exception& e) { TranslateAndThrow(env, e); }
123
+ [[noreturn]] inline void TranslateAndThrow(Napi::Env env, const std::exception& e) {
124
+ if (const auto* fe = dynamic_cast<const FIX::Exception*>(&e)) {
125
+ throw MakeFixError(env, *fe);
126
+ }
127
+ throw MakeStdError(env, e);
128
+ }
129
+
130
+ } // namespace napi_quickfix
131
+
132
+ // Wrap a QuickFIX-calling body so any std::exception is converted to a JS throw.
133
+ // Usage:
134
+ // NQ_TRY(env) { ...quickfix calls that may throw... } NQ_CATCH(env)
135
+ // The body may `return` a Napi::Value normally.
136
+ #define NQ_TRY(env) try
137
+ #define NQ_CATCH(env) \
138
+ catch (const std::exception& _nq_e) { \
139
+ ::napi_quickfix::TranslateAndThrow((env), _nq_e); \
140
+ }
141
+
142
+ #endif // NAPI_QUICKFIX_ERRORS_H
@@ -0,0 +1,255 @@
1
+ #include "initiator_wrap.h"
2
+
3
+ #include <string>
4
+
5
+ #include "quickfix/FileLog.h"
6
+ #include "quickfix/FileStore.h"
7
+
8
+ #include "engine_common.h"
9
+ #include "engine_workers.h"
10
+ #include "errors.h"
11
+ #include "session_settings_wrap.h"
12
+
13
+ namespace napi_quickfix {
14
+
15
+ Napi::FunctionReference InitiatorWrap::constructor_;
16
+
17
+ namespace {
18
+
19
+ std::string OptString(Napi::Object opts, const char* key,
20
+ const std::string& dflt) {
21
+ if (opts.Has(key)) {
22
+ Napi::Value v = opts.Get(key);
23
+ if (v.IsString()) return v.As<Napi::String>().Utf8Value();
24
+ }
25
+ return dflt;
26
+ }
27
+
28
+ } // namespace
29
+
30
+ Napi::Object InitiatorWrap::Init(Napi::Env env, Napi::Object exports) {
31
+ Napi::Function func = DefineClass(
32
+ env, "Initiator",
33
+ {
34
+ InstanceMethod("start", &InitiatorWrap::Start),
35
+ InstanceMethod("stop", &InitiatorWrap::Stop),
36
+ InstanceMethod("isLoggedOn", &InitiatorWrap::IsLoggedOn),
37
+ InstanceMethod("ref", &InitiatorWrap::Ref),
38
+ InstanceMethod("unref", &InitiatorWrap::Unref),
39
+ });
40
+ constructor_ = Napi::Persistent(func);
41
+ constructor_.SuppressDestruct();
42
+ exports.Set("InitiatorWrap", func);
43
+ return exports;
44
+ }
45
+
46
+ InitiatorWrap::InitiatorWrap(const Napi::CallbackInfo& info)
47
+ : Napi::ObjectWrap<InitiatorWrap>(info) {
48
+ Napi::Env env = info.Env();
49
+ if (info.Length() < 1 || !info[0].IsObject()) {
50
+ throw Napi::TypeError::New(
51
+ env, "new Initiator({ settings, handlers?, store?, log? })");
52
+ }
53
+ Napi::Object opts = info[0].As<Napi::Object>();
54
+
55
+ SessionSettingsWrap* settingsWrap =
56
+ SessionSettingsWrap::UnwrapArg(env, opts.Get("settings"), "settings");
57
+
58
+ Napi::Object handlers;
59
+ if (opts.Has("handlers") && opts.Get("handlers").IsObject()) {
60
+ handlers = opts.Get("handlers").As<Napi::Object>();
61
+ } else {
62
+ handlers = Napi::Object::New(env);
63
+ }
64
+
65
+ std::string store = OptString(opts, "store", "file");
66
+ std::string log = OptString(opts, "log", "screen");
67
+
68
+ const FIX::SessionSettings& settings = settingsWrap->Settings();
69
+
70
+ // Bridge must be created on the JS thread (captures env / handlers).
71
+ bridge_ = std::make_unique<ApplicationBridge>(env, handlers);
72
+
73
+ NQ_TRY(env) {
74
+ if (store == "memory") {
75
+ storeFactory_ = std::make_unique<FIX::MemoryStoreFactory>();
76
+ } else {
77
+ storeFactory_ = std::make_unique<FIX::FileStoreFactory>(settings);
78
+ }
79
+
80
+ if (log == "none") {
81
+ logFactory_ = std::make_unique<NullLogFactory>();
82
+ } else if (log == "file") {
83
+ logFactory_ = std::make_unique<FIX::FileLogFactory>(settings);
84
+ } else {
85
+ logFactory_ = std::make_unique<FIX::ScreenLogFactory>(settings);
86
+ }
87
+
88
+ initiator_ = std::make_unique<FIX::SocketInitiator>(
89
+ *bridge_, *storeFactory_, settings, *logFactory_);
90
+ } NQ_CATCH(env)
91
+
92
+ // Register a cleanup hook so teardown runs deterministically on the MAIN
93
+ // thread during environment shutdown, BEFORE the TSFN is finalized. ObjectWrap
94
+ // finalizers do NOT reliably run before env teardown when the loop is
95
+ // unref'd, so relying on the destructor alone lets a live QuickFIX network
96
+ // thread call into an already-destroyed TSFN and abort the process.
97
+ env_ = env;
98
+ cleanupHook_ = env.AddCleanupHook(&InitiatorWrap::CleanupEntry, this);
99
+ }
100
+
101
+ void InitiatorWrap::CleanupEntry(InitiatorWrap* self) {
102
+ // Runs on the MAIN thread during env shutdown. Join network threads and tear
103
+ // down the bridge before N-API destroys the TSFN. Mark fired so the later
104
+ // destructor doesn't Remove() an already-freed cleanup hook (double free).
105
+ self->cleanupHookFired_ = true;
106
+ self->Teardown(true);
107
+ }
108
+
109
+ InitiatorWrap::~InitiatorWrap() {
110
+ // If the object is GC-finalized BEFORE env shutdown, remove the cleanup hook
111
+ // so it won't fire on freed memory. If the hook already fired, node already
112
+ // freed the hook data — do NOT Remove() (that would double-free).
113
+ if (!cleanupHookFired_ && !cleanupHook_.IsEmpty() && env_ != nullptr) {
114
+ cleanupHook_.Remove(env_);
115
+ }
116
+ Teardown(true);
117
+ }
118
+
119
+ void InitiatorWrap::Teardown(bool force) {
120
+ // Runs on the MAIN thread (finalizer / explicit teardown). MUST NOT block on a
121
+ // JS round-trip and MUST NOT deadlock. Ordering matters:
122
+ // 1. Deactivate the bridge (set a flag, no TSFN op): any callback firing on
123
+ // a QuickFIX network thread during the upcoming stop() will pass through
124
+ // immediately without touching the TSFN or waiting on the (frozen) JS
125
+ // loop. This breaks the stop()->onLogout->BlockingCall deadlock.
126
+ // 2. stop(force): joins all QuickFIX network threads. After it returns, no
127
+ // network thread is running, so none can race into the TSFN.
128
+ // 3. Abort the TSFN: safe now that no producer thread remains.
129
+ if (stopped_) return;
130
+ stopped_ = true;
131
+ if (bridge_) {
132
+ bridge_->Deactivate();
133
+ }
134
+ if (initiator_ && started_) {
135
+ try {
136
+ initiator_->stop(force);
137
+ } catch (...) {
138
+ }
139
+ }
140
+ if (bridge_) {
141
+ bridge_->Abort();
142
+ }
143
+ }
144
+
145
+ Napi::Value InitiatorWrap::Start(const Napi::CallbackInfo& info) {
146
+ Napi::Env env = info.Env();
147
+ auto deferred = Napi::Promise::Deferred::New(env);
148
+ if (stopped_) {
149
+ deferred.Reject(Napi::Error::New(env, "Initiator has been stopped").Value());
150
+ return deferred.Promise();
151
+ }
152
+ if (started_) {
153
+ deferred.Resolve(env.Undefined());
154
+ return deferred.Promise();
155
+ }
156
+ if (busy_) {
157
+ deferred.Reject(
158
+ Napi::Error::New(env, "Initiator start/stop already in progress")
159
+ .Value());
160
+ return deferred.Promise();
161
+ }
162
+ busy_ = true;
163
+ started_ = true; // set optimistically; reset in onSettled on failure
164
+
165
+ FIX::SocketInitiator* initiator = initiator_.get();
166
+ auto* self = this;
167
+ auto* worker = new EngineOpWorker(
168
+ env, info.This().As<Napi::Object>(),
169
+ [initiator]() { initiator->start(); },
170
+ [self](bool ok) {
171
+ self->busy_ = false;
172
+ // If start failed, mark not-started so teardown won't try to stop an
173
+ // engine that never came up, and a later start() can retry.
174
+ if (!ok) self->started_ = false;
175
+ });
176
+ Napi::Promise promise = worker->Promise();
177
+ worker->Queue();
178
+ return promise;
179
+ }
180
+
181
+ Napi::Value InitiatorWrap::Stop(const Napi::CallbackInfo& info) {
182
+ Napi::Env env = info.Env();
183
+ bool force = false;
184
+ if (info.Length() >= 1 && !info[0].IsUndefined()) {
185
+ force = info[0].ToBoolean().Value();
186
+ }
187
+ auto deferred = Napi::Promise::Deferred::New(env);
188
+ if (stopped_) {
189
+ deferred.Resolve(env.Undefined());
190
+ return deferred.Promise();
191
+ }
192
+ if (busy_) {
193
+ deferred.Reject(
194
+ Napi::Error::New(env, "Initiator start/stop already in progress")
195
+ .Value());
196
+ return deferred.Promise();
197
+ }
198
+ if (!initiator_ || !started_) {
199
+ // Never started (or already torn down): just release the TSFN and resolve.
200
+ stopped_ = true;
201
+ if (bridge_) bridge_->Release();
202
+ deferred.Resolve(env.Undefined());
203
+ return deferred.Promise();
204
+ }
205
+ busy_ = true;
206
+ stopped_ = true;
207
+
208
+ FIX::SocketInitiator* initiator = initiator_.get();
209
+ ApplicationBridge* bridge = bridge_.get();
210
+ auto* self = this;
211
+ auto* worker = new EngineOpWorker(
212
+ env, info.This().As<Napi::Object>(),
213
+ // stop() runs on the worker thread; its onLogout/toAdmin callbacks fire on
214
+ // QuickFIX threads while the main loop is free to service the TSFN.
215
+ [initiator, force]() { initiator->stop(force); },
216
+ [self, bridge, force](bool /*ok*/) {
217
+ self->busy_ = false;
218
+ // Now that stop() has completed and no more callbacks will fire, drain
219
+ // (or abort) the TSFN so the event loop can exit on its own.
220
+ if (bridge) {
221
+ if (force) {
222
+ bridge->Abort();
223
+ } else {
224
+ bridge->Release();
225
+ }
226
+ }
227
+ });
228
+ Napi::Promise promise = worker->Promise();
229
+ worker->Queue();
230
+ return promise;
231
+ }
232
+
233
+ Napi::Value InitiatorWrap::IsLoggedOn(const Napi::CallbackInfo& info) {
234
+ Napi::Env env = info.Env();
235
+ if (!initiator_ || !started_ || stopped_) {
236
+ return Napi::Boolean::New(env, false);
237
+ }
238
+ NQ_TRY(env) {
239
+ return Napi::Boolean::New(env, initiator_->isLoggedOn());
240
+ } NQ_CATCH(env)
241
+ }
242
+
243
+ Napi::Value InitiatorWrap::Ref(const Napi::CallbackInfo& info) {
244
+ Napi::Env env = info.Env();
245
+ if (bridge_) bridge_->Ref(env);
246
+ return info.This();
247
+ }
248
+
249
+ Napi::Value InitiatorWrap::Unref(const Napi::CallbackInfo& info) {
250
+ Napi::Env env = info.Env();
251
+ if (bridge_) bridge_->Unref(env);
252
+ return info.This();
253
+ }
254
+
255
+ } // namespace napi_quickfix
@@ -0,0 +1,69 @@
1
+ #ifndef NAPI_QUICKFIX_INITIATOR_WRAP_H
2
+ #define NAPI_QUICKFIX_INITIATOR_WRAP_H
3
+
4
+ #include <napi.h>
5
+
6
+ #include <memory>
7
+
8
+ #include "quickfix/Log.h"
9
+ #include "quickfix/MessageStore.h"
10
+ #include "quickfix/SocketInitiator.h"
11
+
12
+ #include "application_bridge.h"
13
+
14
+ namespace napi_quickfix {
15
+
16
+ // Napi::ObjectWrap over FIX::SocketInitiator.
17
+ //
18
+ // new Initiator({ settings, handlers?, store?, log? })
19
+ // store: 'file' | 'memory' (default 'file')
20
+ // log: 'screen' | 'file' | 'none' (default 'screen')
21
+ //
22
+ // Methods: start / stop(force?) / isLoggedOn / ref / unref.
23
+ class InitiatorWrap : public Napi::ObjectWrap<InitiatorWrap> {
24
+ public:
25
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
26
+ explicit InitiatorWrap(const Napi::CallbackInfo& info);
27
+ ~InitiatorWrap() override;
28
+
29
+ private:
30
+ static Napi::FunctionReference constructor_;
31
+
32
+ Napi::Value Start(const Napi::CallbackInfo& info);
33
+ Napi::Value Stop(const Napi::CallbackInfo& info);
34
+ Napi::Value IsLoggedOn(const Napi::CallbackInfo& info);
35
+ Napi::Value Ref(const Napi::CallbackInfo& info);
36
+ Napi::Value Unref(const Napi::CallbackInfo& info);
37
+
38
+ void Teardown(bool force);
39
+
40
+ // Declaration order matters: members destruct in REVERSE order, so the
41
+ // initiator (declared last) is destroyed FIRST, before the factories and
42
+ // bridge it references.
43
+ std::unique_ptr<ApplicationBridge> bridge_;
44
+ std::unique_ptr<FIX::MessageStoreFactory> storeFactory_;
45
+ std::unique_ptr<FIX::LogFactory> logFactory_;
46
+ std::unique_ptr<FIX::SocketInitiator> initiator_;
47
+
48
+ bool started_ = false;
49
+ bool stopped_ = false;
50
+ bool busy_ = false; // guards against overlapping start/stop AsyncWorkers
51
+
52
+ static void CleanupEntry(InitiatorWrap* self);
53
+
54
+ // Env captured at construction (JS thread), used only to remove the cleanup
55
+ // hook if the object is GC-finalized before environment shutdown.
56
+ napi_env env_ = nullptr;
57
+ // Runs Teardown on the MAIN thread during environment shutdown, BEFORE N-API
58
+ // finalizes the TSFN — the only reliable point to join QuickFIX network
59
+ // threads before the TSFN they call into is destroyed. Removed in the
60
+ // destructor ONLY if the hook has not already fired (node's cleanup wrapper
61
+ // frees its own data after invoking the hook, so Remove() afterward would
62
+ // double-free; we track that with cleanupHookFired_).
63
+ Napi::Env::CleanupHook<void (*)(InitiatorWrap*), InitiatorWrap> cleanupHook_;
64
+ bool cleanupHookFired_ = false;
65
+ };
66
+
67
+ } // namespace napi_quickfix
68
+
69
+ #endif // NAPI_QUICKFIX_INITIATOR_WRAP_H