@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,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
@@ -0,0 +1,144 @@
1
+ #ifndef NAPI_QUICKFIX_SESSION_WRAP_H
2
+ #define NAPI_QUICKFIX_SESSION_WRAP_H
3
+
4
+ #include <napi.h>
5
+
6
+ #include <memory>
7
+
8
+ #include "quickfix/Session.h"
9
+ #include "quickfix/SessionID.h"
10
+ #include "session_op_gate.h"
11
+
12
+ namespace napi_quickfix {
13
+
14
+ // Napi::ObjectWrap giving JS a handle on a live FIX::Session.
15
+ //
16
+ // Lifetime: FIX::Session objects are OWNED BY THE ENGINE (Initiator/Acceptor)
17
+ // and FIX::Session::lookupSession returns a raw, non-owning pointer. This wrap
18
+ // therefore never caches that pointer: it stores the FIX::SessionID by value
19
+ // plus the SessionOpGate of the engine that owned the session when the handle
20
+ // was created, and re-resolves on EVERY call, throwing a QuickFixError with
21
+ // fixErrorName 'SessionNotFound' once the engine has been stopped (which
22
+ // destroys its sessions). The gate is what ties the handle to ONE engine: the
23
+ // process-wide registry lookupSession() reads is keyed by SessionID alone, so
24
+ // after the owner is stopped a new engine may register the same id, and a
25
+ // handle that only re-resolved the id would silently operate on the new
26
+ // engine's session. Resolve() therefore also requires the registered owner of
27
+ // the id (SessionOpGate::Lookup) to still be the gate captured at creation.
28
+ //
29
+ // new SessionWrap(sessionID: SessionIDWrap) -- internal; JS reaches a
30
+ // Session through lookupSession() / engine.getSession(), which bind the
31
+ // gate via NewInstance(). A handle built any other way has no gate and
32
+ // always throws SessionNotFound.
33
+ //
34
+ // Every member is synchronous and safe on the JS thread: none takes
35
+ // FIX::Session::m_mutex, which QuickFIX holds across application callbacks.
36
+ // getSessionID, isLoggedOn, isEnabled, sentLogon, sentLogout, receivedLogon,
37
+ // isInitiator, isAcceptor, isSessionTime(nowMs?), isLogonTime(nowMs?),
38
+ // getExpectedSenderNum, getExpectedTargetNum, setNextSenderMsgSeqNum,
39
+ // setNextTargetMsgSeqNum, logon, logout(reason?), refresh, and the runtime
40
+ // option getters/setters.
41
+ // FIX::Session::disconnect()/reset() are not exposed: they are only safe on
42
+ // the engine's own network thread (see session_wrap.cpp).
43
+ class SessionWrap : public Napi::ObjectWrap<SessionWrap> {
44
+ public:
45
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
46
+
47
+ // Build a JS Session bound to a copy of `id` and to `gate`, the
48
+ // SessionOpGate of the engine owning the session (SessionOpGate::Lookup(id)
49
+ // or the engine wrap's own gate_). Does NOT check that the session exists;
50
+ // callers (lookupSession / engine.getSession) do.
51
+ static Napi::Object NewInstance(Napi::Env env, const FIX::SessionID& id,
52
+ std::shared_ptr<SessionOpGate> gate);
53
+
54
+ explicit SessionWrap(const Napi::CallbackInfo& info);
55
+
56
+ const FIX::SessionID& SessionID() const { return id_; }
57
+
58
+ private:
59
+ static Napi::FunctionReference constructor_;
60
+
61
+ // Resolve the live FIX::Session of the engine this handle was created for,
62
+ // or throw FIX::SessionNotFound (session gone, or the id now belongs to a
63
+ // different engine). Must be called inside NQ_TRY so the exception maps to a
64
+ // QuickFixError.
65
+ FIX::Session& Resolve();
66
+
67
+ Napi::Value GetSessionID(const Napi::CallbackInfo& info);
68
+
69
+ Napi::Value IsLoggedOn(const Napi::CallbackInfo& info);
70
+ Napi::Value IsEnabled(const Napi::CallbackInfo& info);
71
+ Napi::Value SentLogon(const Napi::CallbackInfo& info);
72
+ Napi::Value SentLogout(const Napi::CallbackInfo& info);
73
+ Napi::Value ReceivedLogon(const Napi::CallbackInfo& info);
74
+ Napi::Value IsInitiator(const Napi::CallbackInfo& info);
75
+ Napi::Value IsAcceptor(const Napi::CallbackInfo& info);
76
+ Napi::Value IsSessionTime(const Napi::CallbackInfo& info);
77
+ Napi::Value IsLogonTime(const Napi::CallbackInfo& info);
78
+
79
+ Napi::Value GetExpectedSenderNum(const Napi::CallbackInfo& info);
80
+ Napi::Value GetExpectedTargetNum(const Napi::CallbackInfo& info);
81
+ Napi::Value SetNextSenderMsgSeqNum(const Napi::CallbackInfo& info);
82
+ Napi::Value SetNextTargetMsgSeqNum(const Napi::CallbackInfo& info);
83
+
84
+ // Runtime options (plain members on FIX::Session; no locking, no callbacks).
85
+ Napi::Value GetResetOnLogon(const Napi::CallbackInfo& info);
86
+ Napi::Value SetResetOnLogon(const Napi::CallbackInfo& info);
87
+ Napi::Value GetResetOnLogout(const Napi::CallbackInfo& info);
88
+ Napi::Value SetResetOnLogout(const Napi::CallbackInfo& info);
89
+ Napi::Value GetResetOnDisconnect(const Napi::CallbackInfo& info);
90
+ Napi::Value SetResetOnDisconnect(const Napi::CallbackInfo& info);
91
+ Napi::Value GetRefreshOnLogon(const Napi::CallbackInfo& info);
92
+ Napi::Value SetRefreshOnLogon(const Napi::CallbackInfo& info);
93
+ Napi::Value GetCheckCompId(const Napi::CallbackInfo& info);
94
+ Napi::Value SetCheckCompId(const Napi::CallbackInfo& info);
95
+ Napi::Value GetCheckLatency(const Napi::CallbackInfo& info);
96
+ Napi::Value SetCheckLatency(const Napi::CallbackInfo& info);
97
+ Napi::Value GetPersistMessages(const Napi::CallbackInfo& info);
98
+ Napi::Value SetPersistMessages(const Napi::CallbackInfo& info);
99
+ Napi::Value GetSendRedundantResendRequests(const Napi::CallbackInfo& info);
100
+ Napi::Value SetSendRedundantResendRequests(const Napi::CallbackInfo& info);
101
+ Napi::Value GetValidateLengthAndChecksum(const Napi::CallbackInfo& info);
102
+ Napi::Value SetValidateLengthAndChecksum(const Napi::CallbackInfo& info);
103
+ Napi::Value GetSendNextExpectedMsgSeqNum(const Napi::CallbackInfo& info);
104
+ Napi::Value SetSendNextExpectedMsgSeqNum(const Napi::CallbackInfo& info);
105
+ Napi::Value GetIsNonStopSession(const Napi::CallbackInfo& info);
106
+ Napi::Value SetIsNonStopSession(const Napi::CallbackInfo& info);
107
+ Napi::Value GetLogonTimeout(const Napi::CallbackInfo& info);
108
+ Napi::Value SetLogonTimeout(const Napi::CallbackInfo& info);
109
+ Napi::Value GetLogoutTimeout(const Napi::CallbackInfo& info);
110
+ Napi::Value SetLogoutTimeout(const Napi::CallbackInfo& info);
111
+ Napi::Value GetMaxLatency(const Napi::CallbackInfo& info);
112
+ Napi::Value SetMaxLatency(const Napi::CallbackInfo& info);
113
+ Napi::Value GetTimestampPrecision(const Napi::CallbackInfo& info);
114
+ Napi::Value SetTimestampPrecision(const Napi::CallbackInfo& info);
115
+
116
+ // Control. All sync: they only touch SessionState under its own mutex,
117
+ // which QuickFIX never holds across a callback.
118
+ Napi::Value Logon(const Napi::CallbackInfo& info);
119
+ Napi::Value Logout(const Napi::CallbackInfo& info);
120
+ Napi::Value Refresh(const Napi::CallbackInfo& info);
121
+
122
+ // Shared helpers for the one-liner getters/setters above.
123
+ template <typename Fn>
124
+ Napi::Value BoolGet(const Napi::CallbackInfo& info, Fn fn);
125
+ template <typename Fn>
126
+ Napi::Value IntGet(const Napi::CallbackInfo& info, Fn fn);
127
+ template <typename Fn>
128
+ Napi::Value BoolSet(const Napi::CallbackInfo& info, Fn fn, const char* name);
129
+ template <typename Fn>
130
+ Napi::Value IntSet(const Napi::CallbackInfo& info, Fn fn, const char* name);
131
+ Napi::Value TimeCheck(const Napi::CallbackInfo& info, bool logonTime);
132
+ Napi::Value SeqNumSet(const Napi::CallbackInfo& info, bool sender,
133
+ const char* name);
134
+
135
+ FIX::SessionID id_;
136
+ // The owning engine's gate at creation; held by shared_ptr so the object
137
+ // stays alive (and its address unique) as long as the handle does. nullptr
138
+ // for a handle not created through NewInstance().
139
+ std::shared_ptr<SessionOpGate> gate_;
140
+ };
141
+
142
+ } // namespace napi_quickfix
143
+
144
+ #endif // NAPI_QUICKFIX_SESSION_WRAP_H
@@ -0,0 +1,29 @@
1
+ import { Engine, type EngineOptions } from './engine.js';
2
+ export type { EngineOptions } from './engine.js';
3
+ /**
4
+ * A FIX session acceptor (wraps `FIX::SocketAcceptor`). Acceptors listen for
5
+ * inbound connections from initiators as configured in
6
+ * {@link EngineOptions.settings}.
7
+ *
8
+ * Extends {@link Engine} (an {@link import('node:events').EventEmitter}), so you
9
+ * can observe lifecycle/message events in addition to supplying `handlers`.
10
+ * Remember: only `handlers` may mutate outbound messages or reject inbound ones;
11
+ * events are observe-only.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const acceptor = new Acceptor({
16
+ * settings: SessionSettings.fromString(cfg),
17
+ * handlers: {
18
+ * fromApp: (msg, id) => { void sendToTarget(makeReply(msg), id); },
19
+ * },
20
+ * });
21
+ * await acceptor.start();
22
+ * // ... later
23
+ * await acceptor.stop();
24
+ * ```
25
+ */
26
+ export declare class Acceptor extends Engine {
27
+ /** @param options Engine options. */
28
+ constructor(options: EngineOptions);
29
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Acceptor = void 0;
4
+ /**
5
+ * FIX session acceptor (server side — listens for initiators).
6
+ */
7
+ const native_js_1 = require("./native.js");
8
+ const engine_js_1 = require("./engine.js");
9
+ /**
10
+ * A FIX session acceptor (wraps `FIX::SocketAcceptor`). Acceptors listen for
11
+ * inbound connections from initiators as configured in
12
+ * {@link EngineOptions.settings}.
13
+ *
14
+ * Extends {@link Engine} (an {@link import('node:events').EventEmitter}), so you
15
+ * can observe lifecycle/message events in addition to supplying `handlers`.
16
+ * Remember: only `handlers` may mutate outbound messages or reject inbound ones;
17
+ * events are observe-only.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const acceptor = new Acceptor({
22
+ * settings: SessionSettings.fromString(cfg),
23
+ * handlers: {
24
+ * fromApp: (msg, id) => { void sendToTarget(makeReply(msg), id); },
25
+ * },
26
+ * });
27
+ * await acceptor.start();
28
+ * // ... later
29
+ * await acceptor.stop();
30
+ * ```
31
+ */
32
+ class Acceptor extends engine_js_1.Engine {
33
+ /** @param options Engine options. */
34
+ constructor(options) {
35
+ super(native_js_1.native.AcceptorWrap, options);
36
+ }
37
+ }
38
+ exports.Acceptor = Acceptor;