@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,13 +1,20 @@
1
1
  #include <napi.h>
2
2
 
3
+ #include <memory>
4
+ #include <set>
5
+ #include <string>
3
6
  #include <utility>
4
7
 
5
8
  #include "errors.h"
9
+ #include "field_map_ops.h"
6
10
  #include "message_wrap.h"
11
+ #include "quickfix/Exceptions.h"
7
12
  #include "quickfix/Message.h"
8
13
  #include "quickfix/Session.h"
9
14
  #include "quickfix/SessionID.h"
10
15
  #include "session_id_wrap.h"
16
+ #include "session_op_gate.h"
17
+ #include "session_wrap.h"
11
18
 
12
19
  namespace napi_quickfix {
13
20
 
@@ -19,22 +26,46 @@ namespace {
19
26
  // deadlock (main blocked in sendToTarget -> toApp -> BlockingCall -> waits for
20
27
  // main to run CallJs, which it can't).
21
28
  //
22
- // The FIX::Message and FIX::SessionID are COPIED BY VALUE on the JS thread
23
- // before Queue()ing, so Execute() never touches any JS object. toApp may mutate
24
- // the worker's private copy during send — that's fine, it's QuickFIX's business.
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).
25
49
  class SendToTargetWorker : public Napi::AsyncWorker {
26
50
  public:
27
- SendToTargetWorker(Napi::Env env, FIX::Message message, FIX::SessionID id)
51
+ SendToTargetWorker(Napi::Env env, FIX::Message message, FIX::SessionID id,
52
+ std::shared_ptr<SessionOpGate> gate)
28
53
  : Napi::AsyncWorker(env),
29
54
  deferred_(Napi::Promise::Deferred::New(env)),
30
55
  message_(std::move(message)),
31
- id_(std::move(id)) {}
56
+ id_(std::move(id)),
57
+ gate_(std::move(gate)) {}
32
58
 
33
59
  Napi::Promise Promise() { return deferred_.Promise(); }
34
60
 
35
61
  // Worker thread. NO Napi/JS access.
36
62
  void Execute() override {
37
63
  try {
64
+ std::unique_ptr<SessionOpGate::OpScope> inflight =
65
+ SessionOpGate::Acquire(gate_, id_);
66
+ if (!inflight) {
67
+ throw FIX::SessionNotFound(id_.toString());
68
+ }
38
69
  result_ = FIX::Session::sendToTarget(message_, id_);
39
70
  } catch (const std::exception& e) {
40
71
  err_.Capture(e);
@@ -64,30 +95,126 @@ class SendToTargetWorker : public Napi::AsyncWorker {
64
95
  Napi::Promise::Deferred deferred_;
65
96
  FIX::Message message_;
66
97
  FIX::SessionID id_;
98
+ std::shared_ptr<SessionOpGate> gate_;
67
99
  bool result_ = false;
68
100
  CapturedError err_;
69
101
  };
70
102
 
71
- } // namespace
72
-
73
103
  // sendToTarget(message: Message, sessionID: SessionID): Promise<boolean>
74
- static Napi::Value SendToTarget(const Napi::CallbackInfo& info) {
104
+ // sendToTarget(message: Message, qualifier?: string): Promise<boolean>
105
+ Napi::Value SendToTarget(const Napi::CallbackInfo& info) {
75
106
  Napi::Env env = info.Env();
76
107
  MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
77
- SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[1], "sessionID");
78
108
 
79
- // Copy the message and session id by value on the JS thread so the worker
109
+ // Copy the message (and session id) by value on the JS thread so the worker
80
110
  // never touches JS-owned objects.
81
- auto* worker =
82
- new SendToTargetWorker(env, msg->Message(), id->SessionID());
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));
83
150
  Napi::Promise promise = worker->Promise();
84
151
  worker->Queue();
85
152
  return promise;
86
153
  }
87
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
+
88
207
  void RegisterSessionStatic(Napi::Env env, Napi::Object exports) {
89
208
  exports.Set("sendToTarget",
90
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"));
91
218
  }
92
219
 
93
220
  } // namespace napi_quickfix
@@ -0,0 +1,438 @@
1
+ #include "session_wrap.h"
2
+
3
+ #include <cmath>
4
+ #include <ctime>
5
+ #include <limits>
6
+ #include <memory>
7
+ #include <string>
8
+ #include <utility>
9
+
10
+ #include "errors.h"
11
+ #include "quickfix/Exceptions.h"
12
+ #include "quickfix/FieldTypes.h"
13
+ #include "session_id_wrap.h"
14
+
15
+ namespace napi_quickfix {
16
+
17
+ Napi::FunctionReference SessionWrap::constructor_;
18
+
19
+ Napi::Object SessionWrap::Init(Napi::Env env, Napi::Object exports) {
20
+ Napi::Function func = DefineClass(
21
+ env, "Session",
22
+ {
23
+ InstanceMethod("getSessionID", &SessionWrap::GetSessionID),
24
+
25
+ InstanceMethod("isLoggedOn", &SessionWrap::IsLoggedOn),
26
+ InstanceMethod("isEnabled", &SessionWrap::IsEnabled),
27
+ InstanceMethod("sentLogon", &SessionWrap::SentLogon),
28
+ InstanceMethod("sentLogout", &SessionWrap::SentLogout),
29
+ InstanceMethod("receivedLogon", &SessionWrap::ReceivedLogon),
30
+ InstanceMethod("isInitiator", &SessionWrap::IsInitiator),
31
+ InstanceMethod("isAcceptor", &SessionWrap::IsAcceptor),
32
+ InstanceMethod("isSessionTime", &SessionWrap::IsSessionTime),
33
+ InstanceMethod("isLogonTime", &SessionWrap::IsLogonTime),
34
+
35
+ InstanceMethod("getExpectedSenderNum",
36
+ &SessionWrap::GetExpectedSenderNum),
37
+ InstanceMethod("getExpectedTargetNum",
38
+ &SessionWrap::GetExpectedTargetNum),
39
+ InstanceMethod("setNextSenderMsgSeqNum",
40
+ &SessionWrap::SetNextSenderMsgSeqNum),
41
+ InstanceMethod("setNextTargetMsgSeqNum",
42
+ &SessionWrap::SetNextTargetMsgSeqNum),
43
+
44
+ InstanceMethod("getResetOnLogon", &SessionWrap::GetResetOnLogon),
45
+ InstanceMethod("setResetOnLogon", &SessionWrap::SetResetOnLogon),
46
+ InstanceMethod("getResetOnLogout", &SessionWrap::GetResetOnLogout),
47
+ InstanceMethod("setResetOnLogout", &SessionWrap::SetResetOnLogout),
48
+ InstanceMethod("getResetOnDisconnect",
49
+ &SessionWrap::GetResetOnDisconnect),
50
+ InstanceMethod("setResetOnDisconnect",
51
+ &SessionWrap::SetResetOnDisconnect),
52
+ InstanceMethod("getRefreshOnLogon", &SessionWrap::GetRefreshOnLogon),
53
+ InstanceMethod("setRefreshOnLogon", &SessionWrap::SetRefreshOnLogon),
54
+ InstanceMethod("getCheckCompId", &SessionWrap::GetCheckCompId),
55
+ InstanceMethod("setCheckCompId", &SessionWrap::SetCheckCompId),
56
+ InstanceMethod("getCheckLatency", &SessionWrap::GetCheckLatency),
57
+ InstanceMethod("setCheckLatency", &SessionWrap::SetCheckLatency),
58
+ InstanceMethod("getPersistMessages",
59
+ &SessionWrap::GetPersistMessages),
60
+ InstanceMethod("setPersistMessages",
61
+ &SessionWrap::SetPersistMessages),
62
+ InstanceMethod("getSendRedundantResendRequests",
63
+ &SessionWrap::GetSendRedundantResendRequests),
64
+ InstanceMethod("setSendRedundantResendRequests",
65
+ &SessionWrap::SetSendRedundantResendRequests),
66
+ InstanceMethod("getValidateLengthAndChecksum",
67
+ &SessionWrap::GetValidateLengthAndChecksum),
68
+ InstanceMethod("setValidateLengthAndChecksum",
69
+ &SessionWrap::SetValidateLengthAndChecksum),
70
+ InstanceMethod("getSendNextExpectedMsgSeqNum",
71
+ &SessionWrap::GetSendNextExpectedMsgSeqNum),
72
+ InstanceMethod("setSendNextExpectedMsgSeqNum",
73
+ &SessionWrap::SetSendNextExpectedMsgSeqNum),
74
+ InstanceMethod("getIsNonStopSession",
75
+ &SessionWrap::GetIsNonStopSession),
76
+ InstanceMethod("setIsNonStopSession",
77
+ &SessionWrap::SetIsNonStopSession),
78
+ InstanceMethod("getLogonTimeout", &SessionWrap::GetLogonTimeout),
79
+ InstanceMethod("setLogonTimeout", &SessionWrap::SetLogonTimeout),
80
+ InstanceMethod("getLogoutTimeout", &SessionWrap::GetLogoutTimeout),
81
+ InstanceMethod("setLogoutTimeout", &SessionWrap::SetLogoutTimeout),
82
+ InstanceMethod("getMaxLatency", &SessionWrap::GetMaxLatency),
83
+ InstanceMethod("setMaxLatency", &SessionWrap::SetMaxLatency),
84
+ InstanceMethod("getTimestampPrecision",
85
+ &SessionWrap::GetTimestampPrecision),
86
+ InstanceMethod("setTimestampPrecision",
87
+ &SessionWrap::SetTimestampPrecision),
88
+
89
+ InstanceMethod("logon", &SessionWrap::Logon),
90
+ InstanceMethod("logout", &SessionWrap::Logout),
91
+ InstanceMethod("refresh", &SessionWrap::Refresh),
92
+ });
93
+
94
+ constructor_ = Napi::Persistent(func);
95
+ constructor_.SuppressDestruct();
96
+
97
+ exports.Set("SessionWrap", func);
98
+ return exports;
99
+ }
100
+
101
+ Napi::Object SessionWrap::NewInstance(Napi::Env env, const FIX::SessionID& id,
102
+ std::shared_ptr<SessionOpGate> gate) {
103
+ Napi::Object obj = constructor_.New({SessionIDWrap::NewInstance(env, id)});
104
+ Unwrap(obj)->gate_ = std::move(gate);
105
+ return obj;
106
+ }
107
+
108
+ SessionWrap::SessionWrap(const Napi::CallbackInfo& info)
109
+ : Napi::ObjectWrap<SessionWrap>(info) {
110
+ Napi::Env env = info.Env();
111
+ if (info.Length() < 1) {
112
+ throw Napi::TypeError::New(
113
+ env,
114
+ "Session cannot be constructed directly; use lookupSession() or "
115
+ "engine.getSession()");
116
+ }
117
+ // Copy by value: the wrap must outlive any SessionID JS object.
118
+ id_ = SessionIDWrap::UnwrapArg(env, info[0], "sessionID")->SessionID();
119
+ }
120
+
121
+ // Engines are created and destroyed on the JS thread only, and so is this
122
+ // call, so the owner check and the lookup cannot interleave with an engine
123
+ // stop. During a graceful stop() the gate stays registered until the engine is
124
+ // destroyed (Freeze() -> Unregister() -> delete, all before stop() settles), so
125
+ // a 'logout' listener fired by the stop still resolves the session.
126
+ FIX::Session& SessionWrap::Resolve() {
127
+ if (!gate_ || SessionOpGate::Lookup(id_) != gate_) {
128
+ throw FIX::SessionNotFound(id_.toString());
129
+ }
130
+ FIX::Session* session = FIX::Session::lookupSession(id_);
131
+ if (session == nullptr) {
132
+ throw FIX::SessionNotFound(id_.toString());
133
+ }
134
+ return *session;
135
+ }
136
+
137
+ Napi::Value SessionWrap::GetSessionID(const Napi::CallbackInfo& info) {
138
+ return SessionIDWrap::NewInstance(info.Env(), id_);
139
+ }
140
+
141
+ // --- generic helpers ---------------------------------------------------------
142
+
143
+ template <typename Fn>
144
+ Napi::Value SessionWrap::BoolGet(const Napi::CallbackInfo& info, Fn fn) {
145
+ Napi::Env env = info.Env();
146
+ NQ_TRY(env) {
147
+ return Napi::Boolean::New(env, (Resolve().*fn)());
148
+ } NQ_CATCH(env)
149
+ }
150
+
151
+ template <typename Fn>
152
+ Napi::Value SessionWrap::IntGet(const Napi::CallbackInfo& info, Fn fn) {
153
+ Napi::Env env = info.Env();
154
+ NQ_TRY(env) {
155
+ return Napi::Number::New(env, static_cast<double>((Resolve().*fn)()));
156
+ } NQ_CATCH(env)
157
+ }
158
+
159
+ template <typename Fn>
160
+ Napi::Value SessionWrap::BoolSet(const Napi::CallbackInfo& info, Fn fn,
161
+ const char* name) {
162
+ Napi::Env env = info.Env();
163
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
164
+ throw Napi::TypeError::New(env, std::string(name) + "(value: boolean)");
165
+ }
166
+ const bool value = info[0].As<Napi::Boolean>().Value();
167
+ NQ_TRY(env) {
168
+ (Resolve().*fn)(value);
169
+ return env.Undefined();
170
+ } NQ_CATCH(env)
171
+ }
172
+
173
+ // The FIX::Session setters take a plain `int`; a double outside its range is
174
+ // undefined behaviour to cast (and on x86-64 lands on INT_MIN, which would e.g.
175
+ // make a MaxLatency reject every inbound message). Bound it here and report
176
+ // a RangeError, matching SeqNumSet / SetTimestampPrecision.
177
+ template <typename Fn>
178
+ Napi::Value SessionWrap::IntSet(const Napi::CallbackInfo& info, Fn fn,
179
+ const char* name) {
180
+ Napi::Env env = info.Env();
181
+ if (info.Length() < 1 || !info[0].IsNumber()) {
182
+ throw Napi::TypeError::New(env, std::string(name) + "(value: number)");
183
+ }
184
+ const double raw = info[0].As<Napi::Number>().DoubleValue();
185
+ constexpr double kIntMin = static_cast<double>(std::numeric_limits<int>::min());
186
+ constexpr double kIntMax = static_cast<double>(std::numeric_limits<int>::max());
187
+ if (!std::isfinite(raw) || std::floor(raw) != raw || raw < kIntMin ||
188
+ raw > kIntMax) {
189
+ throw Napi::RangeError::New(
190
+ env, std::string(name) + ": value must be an integer in the int32 range");
191
+ }
192
+ NQ_TRY(env) {
193
+ (Resolve().*fn)(static_cast<int>(raw));
194
+ return env.Undefined();
195
+ } NQ_CATCH(env)
196
+ }
197
+
198
+ // isSessionTime(nowMs?) / isLogonTime(nowMs?): `nowMs` is an epoch-millisecond
199
+ // number (Date.getTime()); omitted => the engine's current UTC clock.
200
+ Napi::Value SessionWrap::TimeCheck(const Napi::CallbackInfo& info,
201
+ bool logonTime) {
202
+ Napi::Env env = info.Env();
203
+ bool hasNow = info.Length() >= 1 && !info[0].IsUndefined();
204
+ double nowMs = 0;
205
+ if (hasNow) {
206
+ if (!info[0].IsNumber()) {
207
+ throw Napi::TypeError::New(
208
+ env, logonTime ? "isLogonTime(nowMs?: number)"
209
+ : "isSessionTime(nowMs?: number)");
210
+ }
211
+ nowMs = info[0].As<Napi::Number>().DoubleValue();
212
+ // Bound to the range a JS Date can hold (+/- 100M days around the epoch)
213
+ // before the floating-to-integer cast below: converting a finite double
214
+ // that does not fit in time_t is undefined behaviour, not a saturation.
215
+ constexpr double kMaxDateMs = 8.64e15;
216
+ if (!std::isfinite(nowMs) || std::fabs(nowMs) > kMaxDateMs) {
217
+ throw Napi::RangeError::New(
218
+ env, "nowMs must be an epoch-millisecond number in the Date range");
219
+ }
220
+ }
221
+ NQ_TRY(env) {
222
+ FIX::Session& session = Resolve();
223
+ FIX::UtcTimeStamp now = FIX::UtcTimeStamp::now();
224
+ if (hasNow) {
225
+ const double secs = std::floor(nowMs / 1000.0);
226
+ const int millis = static_cast<int>(nowMs - secs * 1000.0);
227
+ now = FIX::UtcTimeStamp(static_cast<time_t>(secs), millis);
228
+ }
229
+ return Napi::Boolean::New(env, logonTime ? session.isLogonTime(now)
230
+ : session.isSessionTime(now));
231
+ } NQ_CATCH(env)
232
+ }
233
+
234
+ // Sequence numbers are FIX::SEQNUM (uint64_t). JS numbers are exact up to
235
+ // 2^53, which is far beyond any practical MsgSeqNum; reject anything that is
236
+ // not a positive safe integer. MsgSeqNum (tag 34) is 1-based: QuickFIX's own
237
+ // SessionState::reset() and MemoryStore start at 1, and a peer answers 34=0
238
+ // with a Logout ("MsgSeqNum too low").
239
+ Napi::Value SessionWrap::SeqNumSet(const Napi::CallbackInfo& info, bool sender,
240
+ const char* name) {
241
+ Napi::Env env = info.Env();
242
+ if (info.Length() < 1 || !info[0].IsNumber()) {
243
+ throw Napi::TypeError::New(env, std::string(name) + "(seqNum: number)");
244
+ }
245
+ const double raw = info[0].As<Napi::Number>().DoubleValue();
246
+ constexpr double kMaxSafe = 9007199254740991.0; // Number.MAX_SAFE_INTEGER
247
+ if (!std::isfinite(raw) || std::floor(raw) != raw || raw < 1 ||
248
+ raw > kMaxSafe) {
249
+ throw Napi::RangeError::New(
250
+ env, std::string(name) + ": seqNum must be a positive safe integer");
251
+ }
252
+ const FIX::SEQNUM num = static_cast<FIX::SEQNUM>(raw);
253
+ NQ_TRY(env) {
254
+ // These touch the MessageStore (may throw IOException) but take only the
255
+ // SessionState mutex, which QuickFIX never holds across a callback -- that
256
+ // is what makes them safe to run here. It also means they are NOT
257
+ // serialised against the engine: Session::sendRaw holds m_mutex across
258
+ // fill() (reads the next sender num into the header), toApp/toAdmin and
259
+ // persist() (incrNextSenderMsgSeqNum from the store), and Session::next()
260
+ // likewise across the target-num verification, fromApp/fromAdmin and
261
+ // incrNextTargetMsgSeqNum. A setter landing inside that window -- from a
262
+ // handler, or while a sendToTarget() / heartbeat is in flight on this
263
+ // session -- is overwritten by the increment that follows, and the wire
264
+ // is left one off from the peer. The setters are meant for a quiescent
265
+ // session (before start(), from a 'logout' event, or when the caller
266
+ // knows nothing is in flight); documented on the JS API, not fixable here
267
+ // (m_mutex is private and must not be taken from the JS thread anyway).
268
+ if (sender) {
269
+ Resolve().setNextSenderMsgSeqNum(num);
270
+ } else {
271
+ Resolve().setNextTargetMsgSeqNum(num);
272
+ }
273
+ return env.Undefined();
274
+ } NQ_CATCH(env)
275
+ }
276
+
277
+ // --- state getters -----------------------------------------------------------
278
+
279
+ Napi::Value SessionWrap::IsLoggedOn(const Napi::CallbackInfo& info) {
280
+ return BoolGet(info, &FIX::Session::isLoggedOn);
281
+ }
282
+ Napi::Value SessionWrap::IsEnabled(const Napi::CallbackInfo& info) {
283
+ return BoolGet(info, &FIX::Session::isEnabled);
284
+ }
285
+ Napi::Value SessionWrap::SentLogon(const Napi::CallbackInfo& info) {
286
+ return BoolGet(info, &FIX::Session::sentLogon);
287
+ }
288
+ Napi::Value SessionWrap::SentLogout(const Napi::CallbackInfo& info) {
289
+ return BoolGet(info, &FIX::Session::sentLogout);
290
+ }
291
+ Napi::Value SessionWrap::ReceivedLogon(const Napi::CallbackInfo& info) {
292
+ return BoolGet(info, &FIX::Session::receivedLogon);
293
+ }
294
+ Napi::Value SessionWrap::IsInitiator(const Napi::CallbackInfo& info) {
295
+ return BoolGet(info, &FIX::Session::isInitiator);
296
+ }
297
+ Napi::Value SessionWrap::IsAcceptor(const Napi::CallbackInfo& info) {
298
+ return BoolGet(info, &FIX::Session::isAcceptor);
299
+ }
300
+ Napi::Value SessionWrap::IsSessionTime(const Napi::CallbackInfo& info) {
301
+ return TimeCheck(info, /*logonTime=*/false);
302
+ }
303
+ Napi::Value SessionWrap::IsLogonTime(const Napi::CallbackInfo& info) {
304
+ return TimeCheck(info, /*logonTime=*/true);
305
+ }
306
+
307
+ // --- sequence numbers --------------------------------------------------------
308
+
309
+ Napi::Value SessionWrap::GetExpectedSenderNum(const Napi::CallbackInfo& info) {
310
+ return IntGet(info, &FIX::Session::getExpectedSenderNum);
311
+ }
312
+ Napi::Value SessionWrap::GetExpectedTargetNum(const Napi::CallbackInfo& info) {
313
+ return IntGet(info, &FIX::Session::getExpectedTargetNum);
314
+ }
315
+ Napi::Value SessionWrap::SetNextSenderMsgSeqNum(
316
+ const Napi::CallbackInfo& info) {
317
+ return SeqNumSet(info, /*sender=*/true, "setNextSenderMsgSeqNum");
318
+ }
319
+ Napi::Value SessionWrap::SetNextTargetMsgSeqNum(
320
+ const Napi::CallbackInfo& info) {
321
+ return SeqNumSet(info, /*sender=*/false, "setNextTargetMsgSeqNum");
322
+ }
323
+
324
+ // --- runtime options ---------------------------------------------------------
325
+
326
+ #define NQ_BOOL_OPTION(Name) \
327
+ Napi::Value SessionWrap::Get##Name(const Napi::CallbackInfo& info) { \
328
+ return BoolGet(info, &FIX::Session::get##Name); \
329
+ } \
330
+ Napi::Value SessionWrap::Set##Name(const Napi::CallbackInfo& info) { \
331
+ return BoolSet(info, &FIX::Session::set##Name, "set" #Name); \
332
+ }
333
+
334
+ #define NQ_INT_OPTION(Name) \
335
+ Napi::Value SessionWrap::Get##Name(const Napi::CallbackInfo& info) { \
336
+ return IntGet(info, &FIX::Session::get##Name); \
337
+ } \
338
+ Napi::Value SessionWrap::Set##Name(const Napi::CallbackInfo& info) { \
339
+ return IntSet(info, &FIX::Session::set##Name, "set" #Name); \
340
+ }
341
+
342
+ NQ_BOOL_OPTION(ResetOnLogon)
343
+ NQ_BOOL_OPTION(ResetOnLogout)
344
+ NQ_BOOL_OPTION(ResetOnDisconnect)
345
+ NQ_BOOL_OPTION(RefreshOnLogon)
346
+ NQ_BOOL_OPTION(CheckCompId)
347
+ NQ_BOOL_OPTION(CheckLatency)
348
+ NQ_BOOL_OPTION(PersistMessages)
349
+ NQ_BOOL_OPTION(SendRedundantResendRequests)
350
+ NQ_BOOL_OPTION(ValidateLengthAndChecksum)
351
+ NQ_BOOL_OPTION(SendNextExpectedMsgSeqNum)
352
+ NQ_BOOL_OPTION(IsNonStopSession)
353
+ NQ_INT_OPTION(LogonTimeout)
354
+ NQ_INT_OPTION(LogoutTimeout)
355
+ NQ_INT_OPTION(MaxLatency)
356
+
357
+ #undef NQ_BOOL_OPTION
358
+ #undef NQ_INT_OPTION
359
+
360
+ Napi::Value SessionWrap::GetTimestampPrecision(const Napi::CallbackInfo& info) {
361
+ return IntGet(info, &FIX::Session::getTimestampPrecision);
362
+ }
363
+
364
+ // FIX::Session::setTimestampPrecision silently ignores values outside 0..9;
365
+ // surface that as a RangeError instead of a no-op.
366
+ Napi::Value SessionWrap::SetTimestampPrecision(const Napi::CallbackInfo& info) {
367
+ Napi::Env env = info.Env();
368
+ if (info.Length() < 1 || !info[0].IsNumber()) {
369
+ throw Napi::TypeError::New(env, "setTimestampPrecision(precision: number)");
370
+ }
371
+ const double raw = info[0].As<Napi::Number>().DoubleValue();
372
+ if (!std::isfinite(raw) || std::floor(raw) != raw || raw < 0 || raw > 9) {
373
+ throw Napi::RangeError::New(
374
+ env, "setTimestampPrecision: precision must be an integer in 0..9");
375
+ }
376
+ NQ_TRY(env) {
377
+ Resolve().setTimestampPrecision(static_cast<int>(raw));
378
+ return env.Undefined();
379
+ } NQ_CATCH(env)
380
+ }
381
+
382
+ // --- control ------------------------------------------------------------------
383
+ // FIX::Session::logon()/logout() only flip SessionState's `enabled` flag and
384
+ // `logoutReason` (the latter under SessionState's own mutex), and refresh()
385
+ // re-reads the store under that same mutex; none of them take
386
+ // FIX::Session::m_mutex or fire a callback, so they cannot deadlock the JS
387
+ // thread against a QuickFIX thread blocked in a handler. That is the property
388
+ // that lets them run here. What they are NOT is race-free: `enabled` is a
389
+ // plain bool that SessionState::enabled(bool) stores with no lock and that
390
+ // the engine's network thread reads in Session::next() under Session::m_mutex
391
+ // -- formally a data race (ThreadSanitizer reports it), the same one
392
+ // FIX::Initiator::stop() makes when it calls logout()/logon() on every session
393
+ // from the stopping thread. In practice the network thread observes the flag
394
+ // on its next pass; this binding does not patch QuickFIX to make it atomic.
395
+ // Keeping them synchronous also keeps them ORDERED: as AsyncWorkers,
396
+ // `logout(); logon();` could have run on two pool threads in either order.
397
+ //
398
+ // FIX::Session::disconnect() and reset() are deliberately NOT exposed. Through
399
+ // the session's responder they reach FIX::SocketMonitor::drop(), which mutates
400
+ // the monitor's socket sets with no lock while the engine's network thread
401
+ // iterates them in SocketMonitor::block(); QuickFIX only ever calls them from
402
+ // that thread itself (upstream issue #346, closed "not planned"). Any thread
403
+ // this binding could run them on -- JS or libuv -- races it. logout() is the
404
+ // supported way to bring a session down from the application.
405
+
406
+ Napi::Value SessionWrap::Logon(const Napi::CallbackInfo& info) {
407
+ Napi::Env env = info.Env();
408
+ NQ_TRY(env) {
409
+ Resolve().logon();
410
+ return env.Undefined();
411
+ } NQ_CATCH(env)
412
+ }
413
+
414
+ Napi::Value SessionWrap::Logout(const Napi::CallbackInfo& info) {
415
+ Napi::Env env = info.Env();
416
+ std::string reason;
417
+ if (info.Length() >= 1 && !info[0].IsUndefined()) {
418
+ if (!info[0].IsString()) {
419
+ throw Napi::TypeError::New(env, "logout(reason?: string)");
420
+ }
421
+ reason = info[0].As<Napi::String>().Utf8Value();
422
+ }
423
+ NQ_TRY(env) {
424
+ Resolve().logout(reason);
425
+ return env.Undefined();
426
+ } NQ_CATCH(env)
427
+ }
428
+
429
+ // May throw IOException from the message store (mapped to QuickFixError).
430
+ Napi::Value SessionWrap::Refresh(const Napi::CallbackInfo& info) {
431
+ Napi::Env env = info.Env();
432
+ NQ_TRY(env) {
433
+ Resolve().refresh();
434
+ return env.Undefined();
435
+ } NQ_CATCH(env)
436
+ }
437
+
438
+ } // namespace napi_quickfix