@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,201 @@
1
+ #ifndef NAPI_QUICKFIX_SESSION_OP_GATE_H
2
+ #define NAPI_QUICKFIX_SESSION_OP_GATE_H
3
+
4
+ #include <condition_variable>
5
+ #include <map>
6
+ #include <memory>
7
+ #include <mutex>
8
+ #include <set>
9
+ #include <utility>
10
+
11
+ #include "quickfix/SessionID.h"
12
+
13
+ namespace napi_quickfix {
14
+
15
+ // Per-engine gate between the libuv-thread session operations
16
+ // (SendToTargetWorker, session_static.cpp) and the destruction of the engine
17
+ // that owns the sessions they operate on.
18
+ //
19
+ // Why: FIX::Session::lookupSession() hands out a raw, non-owning pointer into
20
+ // an engine-owned object, and nothing in QuickFIX keeps that session alive
21
+ // while the caller uses it. The engine wraps destroy their FIX engine (which
22
+ // deletes every FIX::Session) as soon as stop() settles, so without this gate
23
+ // a `sendToTarget()` still running on one libuv thread while `engine.stop()`
24
+ // completes would be a use-after-free.
25
+ //
26
+ // Why per engine (not process-wide): the gate must only ever couple an
27
+ // operation with the engine that owns its session. A process-wide gate made
28
+ // `A.stop()` wait for B's in-flight operations, and -- worse -- made the wrap
29
+ // destructor, which runs on the JS thread at GC, wait for an operation of
30
+ // ANOTHER engine that may itself be blocked in a BlockingCall that only the JS
31
+ // thread can service: a hard deadlock. With one gate per engine, the
32
+ // destructor only waits for operations on its own sessions, and Teardown()
33
+ // has already deactivated this engine's bridge and joined its network threads
34
+ // by then, so none of those can be waiting on the JS thread.
35
+ //
36
+ // Protocol:
37
+ // * Each engine wrap owns a `std::shared_ptr<SessionOpGate>` and registers
38
+ // it for its SessionIDs (Register) right after constructing the FIX engine,
39
+ // on the JS thread; it unregisters (Unregister) right before destroying it.
40
+ // * The JS-thread entry point of a session operation (SendToTarget) looks
41
+ // the gate of the engine owning `id` up (Lookup) BEFORE queuing its
42
+ // worker, so the operation is bound to the engine that owned the session
43
+ // when it was called: engines register and unregister on the JS thread
44
+ // only, so that lookup is exact, and an engine registered later for the
45
+ // same id can never be the one the operation runs on. No registered gate
46
+ // means no engine owns the session: SessionNotFound, without touching the
47
+ // registry QuickFIX keeps. The worker then calls Acquire(gate, id) on its
48
+ // libuv thread to hold an OpScope on that gate for its whole Execute()
49
+ // -- lookupSession AND the operation. Constructing the OpScope blocks
50
+ // while that engine is between Freeze() and Unfreeze(); a gate that is no
51
+ // longer the registered owner once the scope is held is SessionNotFound
52
+ // too (see below).
53
+ // * The engine's stop worker calls Freeze() on its libuv thread AFTER the FIX
54
+ // engine's own stop() has returned: it blocks new operations on this
55
+ // engine's sessions and waits for the in-flight ones to drain. The wrap
56
+ // then unregisters and destroys the engine on the JS thread (OnOK) and
57
+ // calls Unfreeze() -- or, if the engine survives (the stop threw, or the
58
+ // environment is torn down before a graceful stop's queued destruction
59
+ // runs), calls Unfreeze() alone so that operations parked meanwhile are
60
+ // woken rather than left pinning a libuv thread. Because Freeze() waits
61
+ // on a libuv thread while the JS loop is free, an in-flight operation
62
+ // blocked in a BlockingCall to JS (sendToTarget() -> toApp) can still
63
+ // complete.
64
+ // * The wrap destructor calls Freeze() on the JS thread, but only after
65
+ // Teardown() has deactivated the bridge (waking every pending BlockingCall
66
+ // and making later callbacks pass through) and joined the network threads,
67
+ // so no in-flight operation on THIS engine's sessions can be waiting on the
68
+ // (now blocked) JS thread. Operations on other engines are not involved.
69
+ //
70
+ // The gate outlives its engine: a worker whose gate was resolved just before
71
+ // the engine was unregistered holds it by shared_ptr and passes the (now
72
+ // lifted) freeze. By then a NEW engine may own the same id -- its sessions are what
73
+ // lookupSession() would now return -- while the worker holds the OLD gate, on
74
+ // which the new engine's stop() never waits. Acquire() closes that window by
75
+ // re-reading the registry after the OpScope is held: the scope is kept only if
76
+ // the gate is still the registered owner, and that owner cannot be destroyed
77
+ // while the scope is in flight (Freeze() precedes Unregister() everywhere).
78
+ // Otherwise the operation is reported as SessionNotFound, never retried
79
+ // against the new owner: it was queued for the engine that is now gone, and a
80
+ // message delivered through another engine's session -- not logged on, so
81
+ // persisted in its store and sent on its first logon -- is exactly the
82
+ // cross-over SessionWrap::Resolve() refuses for Session handles. Together
83
+ // with the wraps rejecting a SessionID that is already live in the process,
84
+ // this makes gate == registered owner == the engine whose session
85
+ // lookupSession() returns.
86
+ class SessionOpGate {
87
+ public:
88
+ class OpScope {
89
+ public:
90
+ explicit OpScope(std::shared_ptr<SessionOpGate> gate)
91
+ : gate_(std::move(gate)) {
92
+ std::unique_lock<std::mutex> lock(gate_->mtx_);
93
+ gate_->cv_.wait(lock, [this] { return gate_->frozen_ == 0; });
94
+ ++gate_->inflight_;
95
+ }
96
+ ~OpScope() {
97
+ {
98
+ std::lock_guard<std::mutex> lock(gate_->mtx_);
99
+ --gate_->inflight_;
100
+ }
101
+ gate_->cv_.notify_all();
102
+ }
103
+ OpScope(const OpScope&) = delete;
104
+ OpScope& operator=(const OpScope&) = delete;
105
+
106
+ private:
107
+ std::shared_ptr<SessionOpGate> gate_;
108
+ };
109
+
110
+ SessionOpGate() = default;
111
+ SessionOpGate(const SessionOpGate&) = delete;
112
+ SessionOpGate& operator=(const SessionOpGate&) = delete;
113
+
114
+ // Block new operations on this engine's sessions and wait for the in-flight
115
+ // ones to finish. Nests: the stop worker freezes on its thread and the wrap
116
+ // unfreezes later on the JS thread, and in between the destructor (if the
117
+ // wrap is finalized during environment teardown) freezes again around its
118
+ // own DestroyEngine().
119
+ void Freeze() {
120
+ std::unique_lock<std::mutex> lock(mtx_);
121
+ ++frozen_;
122
+ cv_.wait(lock, [this] { return inflight_ == 0; });
123
+ }
124
+
125
+ // Let operations run again. Pairs with Freeze().
126
+ void Unfreeze() {
127
+ {
128
+ std::lock_guard<std::mutex> lock(mtx_);
129
+ --frozen_;
130
+ }
131
+ cv_.notify_all();
132
+ }
133
+
134
+ // --- registry: SessionID -> owning engine's gate ---------------------------
135
+ // Each id maps to at most one gate at a time: the engine wraps refuse to
136
+ // construct an engine whose SessionID is already live in the process
137
+ // (ConfigError "Duplicate Session"). QuickFIX itself does NOT enforce that --
138
+ // its Session constructor ignores addSession()'s result, so a second engine
139
+ // would silently shadow the first in the process-wide session registry.
140
+
141
+ static void Register(const std::set<FIX::SessionID>& ids,
142
+ const std::shared_ptr<SessionOpGate>& gate) {
143
+ Registry& r = GetRegistry();
144
+ std::lock_guard<std::mutex> lock(r.mtx);
145
+ for (const FIX::SessionID& id : ids) r.gates[id] = gate;
146
+ }
147
+
148
+ // Remove `ids` only where they still point at `gate`: a new engine may
149
+ // already have registered the same ids after this one unregistered them.
150
+ static void Unregister(const std::set<FIX::SessionID>& ids,
151
+ const SessionOpGate* gate) {
152
+ Registry& r = GetRegistry();
153
+ std::lock_guard<std::mutex> lock(r.mtx);
154
+ for (const FIX::SessionID& id : ids) {
155
+ auto it = r.gates.find(id);
156
+ if (it != r.gates.end() && it->second.get() == gate) r.gates.erase(it);
157
+ }
158
+ }
159
+
160
+ // The gate of the engine owning `id`, or nullptr if no engine does.
161
+ static std::shared_ptr<SessionOpGate> Lookup(const FIX::SessionID& id) {
162
+ Registry& r = GetRegistry();
163
+ std::lock_guard<std::mutex> lock(r.mtx);
164
+ auto it = r.gates.find(id);
165
+ return it == r.gates.end() ? nullptr : it->second;
166
+ }
167
+
168
+ // Hold an OpScope on `gate` -- the registered owner of `id` when the
169
+ // caller looked it up on the JS thread -- or return nullptr if it no longer
170
+ // is (-> SessionNotFound). Constructing the scope may block (the owner is
171
+ // stopping); once it is held the registry is re-read, and the scope is kept
172
+ // only if the same gate is still registered. Otherwise the owner was
173
+ // destroyed while we waited -- possibly replaced by a new engine with the
174
+ // same id -- and the operation is refused (nullptr) rather than run against
175
+ // the new owner, mirroring SessionWrap::Resolve().
176
+ static std::unique_ptr<OpScope> Acquire(
177
+ const std::shared_ptr<SessionOpGate>& gate, const FIX::SessionID& id) {
178
+ auto scope = std::make_unique<OpScope>(gate);
179
+ if (Lookup(id) != gate) return nullptr;
180
+ return scope;
181
+ }
182
+
183
+ private:
184
+ struct Registry {
185
+ std::mutex mtx;
186
+ std::map<FIX::SessionID, std::shared_ptr<SessionOpGate>> gates;
187
+ };
188
+ static Registry& GetRegistry() {
189
+ static Registry registry;
190
+ return registry;
191
+ }
192
+
193
+ std::mutex mtx_;
194
+ std::condition_variable cv_;
195
+ int inflight_ = 0;
196
+ int frozen_ = 0;
197
+ };
198
+
199
+ } // namespace napi_quickfix
200
+
201
+ #endif // NAPI_QUICKFIX_SESSION_OP_GATE_H
@@ -0,0 +1,98 @@
1
+ #include "session_settings_wrap.h"
2
+
3
+ #include <set>
4
+ #include <sstream>
5
+ #include <string>
6
+
7
+ #include "errors.h"
8
+ #include "quickfix/SessionID.h"
9
+ #include "session_id_wrap.h"
10
+
11
+ namespace napi_quickfix {
12
+
13
+ Napi::FunctionReference SessionSettingsWrap::constructor_;
14
+
15
+ Napi::Object SessionSettingsWrap::Init(Napi::Env env, Napi::Object exports) {
16
+ Napi::Function func = DefineClass(
17
+ env, "SessionSettings",
18
+ {
19
+ StaticMethod("fromString", &SessionSettingsWrap::FromString),
20
+ StaticMethod("fromFile", &SessionSettingsWrap::FromFile),
21
+ InstanceMethod("getSessions", &SessionSettingsWrap::GetSessions),
22
+ });
23
+
24
+ constructor_ = Napi::Persistent(func);
25
+ constructor_.SuppressDestruct();
26
+
27
+ exports.Set("SessionSettingsWrap", func);
28
+ return exports;
29
+ }
30
+
31
+ SessionSettingsWrap* SessionSettingsWrap::UnwrapArg(Napi::Env env,
32
+ Napi::Value value,
33
+ const char* argName) {
34
+ if (!value.IsObject() ||
35
+ !value.As<Napi::Object>().InstanceOf(constructor_.Value())) {
36
+ throw Napi::TypeError::New(
37
+ env, std::string(argName) + " must be a SessionSettings instance");
38
+ }
39
+ return Napi::ObjectWrap<SessionSettingsWrap>::Unwrap(
40
+ value.As<Napi::Object>());
41
+ }
42
+
43
+ // Constructor is internal: (kind: "string"|"file", payload: string).
44
+ // Users go through the static fromString/fromFile factories.
45
+ SessionSettingsWrap::SessionSettingsWrap(const Napi::CallbackInfo& info)
46
+ : Napi::ObjectWrap<SessionSettingsWrap>(info) {
47
+ Napi::Env env = info.Env();
48
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
49
+ throw Napi::TypeError::New(
50
+ env,
51
+ "SessionSettings cannot be constructed directly; use "
52
+ "SessionSettings.fromString() or SessionSettings.fromFile()");
53
+ }
54
+ std::string kind = info[0].As<Napi::String>().Utf8Value();
55
+ std::string payload = info[1].As<Napi::String>().Utf8Value();
56
+
57
+ NQ_TRY(env) {
58
+ if (kind == "string") {
59
+ std::istringstream stream(payload);
60
+ settings_ = FIX::SessionSettings(stream);
61
+ } else { // "file"
62
+ settings_ = FIX::SessionSettings(payload);
63
+ }
64
+ } NQ_CATCH(env)
65
+ }
66
+
67
+ Napi::Value SessionSettingsWrap::FromString(const Napi::CallbackInfo& info) {
68
+ Napi::Env env = info.Env();
69
+ if (info.Length() < 1 || !info[0].IsString()) {
70
+ throw Napi::TypeError::New(env, "fromString(text: string)");
71
+ }
72
+ return constructor_.New(
73
+ {Napi::String::New(env, "string"), info[0].As<Napi::String>()});
74
+ }
75
+
76
+ Napi::Value SessionSettingsWrap::FromFile(const Napi::CallbackInfo& info) {
77
+ Napi::Env env = info.Env();
78
+ if (info.Length() < 1 || !info[0].IsString()) {
79
+ throw Napi::TypeError::New(env, "fromFile(path: string)");
80
+ }
81
+ return constructor_.New(
82
+ {Napi::String::New(env, "file"), info[0].As<Napi::String>()});
83
+ }
84
+
85
+ Napi::Value SessionSettingsWrap::GetSessions(const Napi::CallbackInfo& info) {
86
+ Napi::Env env = info.Env();
87
+ NQ_TRY(env) {
88
+ std::set<FIX::SessionID> sessions = settings_.getSessions();
89
+ Napi::Array arr = Napi::Array::New(env, sessions.size());
90
+ uint32_t i = 0;
91
+ for (const auto& id : sessions) {
92
+ arr.Set(i++, SessionIDWrap::NewInstance(env, id));
93
+ }
94
+ return arr;
95
+ } NQ_CATCH(env)
96
+ }
97
+
98
+ } // namespace napi_quickfix
@@ -0,0 +1,40 @@
1
+ #ifndef NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
2
+ #define NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
3
+
4
+ #include <napi.h>
5
+
6
+ #include "quickfix/SessionSettings.h"
7
+
8
+ namespace napi_quickfix {
9
+
10
+ // Napi::ObjectWrap over FIX::SessionSettings.
11
+ //
12
+ // SessionSettings.fromString(text): SessionSettings
13
+ // SessionSettings.fromFile(path): SessionSettings
14
+ // settings.getSessions(): SessionID[]
15
+ class SessionSettingsWrap : public Napi::ObjectWrap<SessionSettingsWrap> {
16
+ public:
17
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
18
+
19
+ explicit SessionSettingsWrap(const Napi::CallbackInfo& info);
20
+
21
+ FIX::SessionSettings& Settings() { return settings_; }
22
+ const FIX::SessionSettings& Settings() const { return settings_; }
23
+
24
+ static SessionSettingsWrap* UnwrapArg(Napi::Env env, Napi::Value value,
25
+ const char* argName);
26
+
27
+ private:
28
+ static Napi::FunctionReference constructor_;
29
+
30
+ static Napi::Value FromString(const Napi::CallbackInfo& info);
31
+ static Napi::Value FromFile(const Napi::CallbackInfo& info);
32
+
33
+ Napi::Value GetSessions(const Napi::CallbackInfo& info);
34
+
35
+ FIX::SessionSettings settings_;
36
+ };
37
+
38
+ } // namespace napi_quickfix
39
+
40
+ #endif // NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
@@ -0,0 +1,220 @@
1
+ #include <napi.h>
2
+
3
+ #include <memory>
4
+ #include <set>
5
+ #include <string>
6
+ #include <utility>
7
+
8
+ #include "errors.h"
9
+ #include "field_map_ops.h"
10
+ #include "message_wrap.h"
11
+ #include "quickfix/Exceptions.h"
12
+ #include "quickfix/Message.h"
13
+ #include "quickfix/Session.h"
14
+ #include "quickfix/SessionID.h"
15
+ #include "session_id_wrap.h"
16
+ #include "session_op_gate.h"
17
+ #include "session_wrap.h"
18
+
19
+ namespace napi_quickfix {
20
+
21
+ namespace {
22
+
23
+ // AsyncWorker for FIX::Session::sendToTarget. Runs OFF the JS main thread so the
24
+ // event loop stays free to service the ApplicationBridge TSFN BlockingCall(s)
25
+ // that toApp/toAdmin trigger during the send. Running on the main thread would
26
+ // deadlock (main blocked in sendToTarget -> toApp -> BlockingCall -> waits for
27
+ // main to run CallJs, which it can't).
28
+ //
29
+ // The FIX::Message (a fieldmap::CloneMessage, nested group entries kept
30
+ // typed) and FIX::SessionID are COPIED on the JS thread before Queue()ing, so
31
+ // Execute() never touches any JS object. toApp may mutate the worker's private
32
+ // copy during send — that's fine, it's QuickFIX's business.
33
+ //
34
+ // Two upstream overloads are supported: by explicit SessionID, or by qualifier
35
+ // (QuickFIX then resolves the session from the message's own header fields:
36
+ // BeginString / SenderCompID / TargetCompID).
37
+ //
38
+ // Session::sendToTarget looks the session up in the process-wide registry and
39
+ // uses the raw pointer, so Execute() runs under the owning engine's
40
+ // SessionOpGate::OpScope to keep that engine alive until the send has returned.
41
+ // That gate is resolved by SendToTarget() on the JS thread, before the worker
42
+ // is queued (for the qualifier form after deriving the SessionID from the
43
+ // message header, exactly as the upstream overload does): the send is thereby
44
+ // bound to the engine that owned the session when it was called, and can
45
+ // never run through an engine registered for the same id afterwards -- not
46
+ // even if it sat in the libuv queue across the owner's stop() and that
47
+ // engine's construction. Execute() only re-checks that the gate is still the
48
+ // owner once the scope is held (see session_op_gate.h).
49
+ class SendToTargetWorker : public Napi::AsyncWorker {
50
+ public:
51
+ SendToTargetWorker(Napi::Env env, FIX::Message message, FIX::SessionID id,
52
+ std::shared_ptr<SessionOpGate> gate)
53
+ : Napi::AsyncWorker(env),
54
+ deferred_(Napi::Promise::Deferred::New(env)),
55
+ message_(std::move(message)),
56
+ id_(std::move(id)),
57
+ gate_(std::move(gate)) {}
58
+
59
+ Napi::Promise Promise() { return deferred_.Promise(); }
60
+
61
+ // Worker thread. NO Napi/JS access.
62
+ void Execute() override {
63
+ try {
64
+ std::unique_ptr<SessionOpGate::OpScope> inflight =
65
+ SessionOpGate::Acquire(gate_, id_);
66
+ if (!inflight) {
67
+ throw FIX::SessionNotFound(id_.toString());
68
+ }
69
+ result_ = FIX::Session::sendToTarget(message_, id_);
70
+ } catch (const std::exception& e) {
71
+ err_.Capture(e);
72
+ } catch (...) {
73
+ err_.has = true;
74
+ err_.message = "unknown native error";
75
+ err_.fixErrorName = "Error";
76
+ }
77
+ }
78
+
79
+ void OnOK() override {
80
+ Napi::Env env = Env();
81
+ Napi::HandleScope scope(env);
82
+ if (err_.has) {
83
+ deferred_.Reject(err_.ToError(env).Value());
84
+ } else {
85
+ deferred_.Resolve(Napi::Boolean::New(env, result_));
86
+ }
87
+ }
88
+
89
+ void OnError(const Napi::Error& e) override {
90
+ Napi::HandleScope scope(Env());
91
+ deferred_.Reject(e.Value());
92
+ }
93
+
94
+ private:
95
+ Napi::Promise::Deferred deferred_;
96
+ FIX::Message message_;
97
+ FIX::SessionID id_;
98
+ std::shared_ptr<SessionOpGate> gate_;
99
+ bool result_ = false;
100
+ CapturedError err_;
101
+ };
102
+
103
+ // sendToTarget(message: Message, sessionID: SessionID): Promise<boolean>
104
+ // sendToTarget(message: Message, qualifier?: string): Promise<boolean>
105
+ Napi::Value SendToTarget(const Napi::CallbackInfo& info) {
106
+ Napi::Env env = info.Env();
107
+ MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
108
+
109
+ // Copy the message (and session id) by value on the JS thread so the worker
110
+ // never touches JS-owned objects.
111
+ FIX::Message message = fieldmap::CloneMessage(msg->Message());
112
+ const bool byQualifier = info.Length() < 2 || info[1].IsUndefined() ||
113
+ info[1].IsString();
114
+ FIX::SessionID id;
115
+ if (!byQualifier) {
116
+ id = SessionIDWrap::UnwrapArg(env, info[1], "sessionID")->SessionID();
117
+ }
118
+
119
+ // Resolve the owning engine HERE, on the JS thread (see the worker's
120
+ // comment): a session that no engine owns is refused right away, with the
121
+ // same error the worker reports when the engine goes away under it.
122
+ std::shared_ptr<SessionOpGate> gate;
123
+ try {
124
+ if (byQualifier) {
125
+ // Mirrors FIX::Session::sendToTarget(Message&, const std::string&):
126
+ // a header missing tag 8/49/56 is a SessionNotFound.
127
+ const std::string qualifier =
128
+ info[1].IsString() ? info[1].As<Napi::String>().Utf8Value()
129
+ : std::string();
130
+ try {
131
+ id = message.getSessionID(qualifier);
132
+ } catch (const FIX::FieldNotFound&) {
133
+ throw FIX::SessionNotFound();
134
+ }
135
+ }
136
+ gate = SessionOpGate::Lookup(id);
137
+ if (!gate) {
138
+ throw FIX::SessionNotFound(id.toString());
139
+ }
140
+ } catch (const FIX::Exception& e) {
141
+ CapturedError err;
142
+ err.Capture(e);
143
+ auto deferred = Napi::Promise::Deferred::New(env);
144
+ deferred.Reject(err.ToError(env).Value());
145
+ return deferred.Promise();
146
+ }
147
+
148
+ auto* worker = new SendToTargetWorker(env, std::move(message), std::move(id),
149
+ std::move(gate));
150
+ Napi::Promise promise = worker->Promise();
151
+ worker->Queue();
152
+ return promise;
153
+ }
154
+
155
+ // lookupSession(sessionID: SessionID): Session | undefined
156
+ Napi::Value LookupSession(const Napi::CallbackInfo& info) {
157
+ Napi::Env env = info.Env();
158
+ SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[0], "sessionID");
159
+ NQ_TRY(env) {
160
+ // The gate of the engine owning the id; registered by every wrap
161
+ // constructor right after it created its sessions, unregistered right
162
+ // before they are destroyed, so (both on the JS thread) a session that
163
+ // exists has exactly one. No gate means no session.
164
+ std::shared_ptr<SessionOpGate> gate = SessionOpGate::Lookup(id->SessionID());
165
+ if (!gate || !FIX::Session::doesSessionExist(id->SessionID())) {
166
+ return env.Undefined();
167
+ }
168
+ return SessionWrap::NewInstance(env, id->SessionID(), std::move(gate));
169
+ } NQ_CATCH(env)
170
+ }
171
+
172
+ // doesSessionExist(sessionID: SessionID): boolean
173
+ Napi::Value DoesSessionExist(const Napi::CallbackInfo& info) {
174
+ Napi::Env env = info.Env();
175
+ SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[0], "sessionID");
176
+ NQ_TRY(env) {
177
+ return Napi::Boolean::New(env,
178
+ FIX::Session::doesSessionExist(id->SessionID()));
179
+ } NQ_CATCH(env)
180
+ }
181
+
182
+ // getSessions(): SessionID[] -- every session registered in this process.
183
+ Napi::Value GetSessions(const Napi::CallbackInfo& info) {
184
+ Napi::Env env = info.Env();
185
+ NQ_TRY(env) {
186
+ const std::set<FIX::SessionID> ids = FIX::Session::getSessions();
187
+ Napi::Array out = Napi::Array::New(env, ids.size());
188
+ uint32_t i = 0;
189
+ for (const FIX::SessionID& id : ids) {
190
+ out.Set(i++, SessionIDWrap::NewInstance(env, id));
191
+ }
192
+ return out;
193
+ } NQ_CATCH(env)
194
+ }
195
+
196
+ // numSessions(): number
197
+ Napi::Value NumSessions(const Napi::CallbackInfo& info) {
198
+ Napi::Env env = info.Env();
199
+ NQ_TRY(env) {
200
+ return Napi::Number::New(env,
201
+ static_cast<double>(FIX::Session::numSessions()));
202
+ } NQ_CATCH(env)
203
+ }
204
+
205
+ } // namespace
206
+
207
+ void RegisterSessionStatic(Napi::Env env, Napi::Object exports) {
208
+ exports.Set("sendToTarget",
209
+ Napi::Function::New(env, SendToTarget, "sendToTarget"));
210
+ exports.Set("lookupSession",
211
+ Napi::Function::New(env, LookupSession, "lookupSession"));
212
+ exports.Set("doesSessionExist",
213
+ Napi::Function::New(env, DoesSessionExist, "doesSessionExist"));
214
+ exports.Set("getSessions",
215
+ Napi::Function::New(env, GetSessions, "getSessions"));
216
+ exports.Set("numSessions",
217
+ Napi::Function::New(env, NumSessions, "numSessions"));
218
+ }
219
+
220
+ } // namespace napi_quickfix