@homaiohq/napi-quickfix 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +131 -0
- package/LICENSE +21 -0
- package/README.md +421 -2
- package/THIRD_PARTY_LICENSES +48 -0
- package/cpp/acceptor_wrap.cpp +234 -0
- package/cpp/acceptor_wrap.h +57 -0
- package/cpp/addon.cpp +83 -0
- package/cpp/application_bridge.cpp +382 -0
- package/cpp/application_bridge.h +151 -0
- package/cpp/data_dictionary_wrap.cpp +79 -0
- package/cpp/data_dictionary_wrap.h +37 -0
- package/cpp/engine_common.cpp +5 -0
- package/cpp/engine_common.h +19 -0
- package/cpp/engine_workers.h +86 -0
- package/cpp/enums.cpp +66 -0
- package/cpp/errors.h +142 -0
- package/cpp/initiator_wrap.cpp +255 -0
- package/cpp/initiator_wrap.h +69 -0
- package/cpp/message_wrap.cpp +195 -0
- package/cpp/message_wrap.h +53 -0
- package/cpp/session_id_wrap.cpp +91 -0
- package/cpp/session_id_wrap.h +46 -0
- package/cpp/session_settings_wrap.cpp +98 -0
- package/cpp/session_settings_wrap.h +40 -0
- package/cpp/session_static.cpp +93 -0
- package/dist/cjs/acceptor.d.ts +30 -0
- package/dist/cjs/acceptor.d.ts.map +1 -0
- package/dist/cjs/acceptor.js +39 -0
- package/dist/cjs/acceptor.js.map +1 -0
- package/dist/cjs/application.d.ts +116 -0
- package/dist/cjs/application.d.ts.map +1 -0
- package/dist/cjs/application.js +63 -0
- package/dist/cjs/application.js.map +1 -0
- package/dist/cjs/data-dictionary.d.ts +49 -0
- package/dist/cjs/data-dictionary.d.ts.map +1 -0
- package/dist/cjs/data-dictionary.js +77 -0
- package/dist/cjs/data-dictionary.js.map +1 -0
- package/dist/cjs/engine.d.ts +114 -0
- package/dist/cjs/engine.d.ts.map +1 -0
- package/dist/cjs/engine.js +138 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/enums.d.ts +22 -0
- package/dist/cjs/enums.d.ts.map +1 -0
- package/dist/cjs/enums.js +49 -0
- package/dist/cjs/enums.js.map +1 -0
- package/dist/cjs/generated/fields.d.ts +6123 -0
- package/dist/cjs/generated/fields.d.ts.map +1 -0
- package/dist/cjs/generated/fields.js +6128 -0
- package/dist/cjs/generated/fields.js.map +1 -0
- package/dist/cjs/index.d.ts +47 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/initiator.d.ts +30 -0
- package/dist/cjs/initiator.d.ts.map +1 -0
- package/dist/cjs/initiator.js +39 -0
- package/dist/cjs/initiator.js.map +1 -0
- package/dist/cjs/load-native.cjs +5 -0
- package/dist/cjs/message.d.ts +114 -0
- package/dist/cjs/message.d.ts.map +1 -0
- package/dist/cjs/message.js +162 -0
- package/dist/cjs/message.js.map +1 -0
- package/dist/cjs/native.d.ts +163 -0
- package/dist/cjs/native.d.ts.map +1 -0
- package/dist/cjs/native.js +23 -0
- package/dist/cjs/native.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/session-id.d.ts +40 -0
- package/dist/cjs/session-id.d.ts.map +1 -0
- package/dist/cjs/session-id.js +67 -0
- package/dist/cjs/session-id.js.map +1 -0
- package/dist/cjs/session-settings.d.ts +47 -0
- package/dist/cjs/session-settings.d.ts.map +1 -0
- package/dist/cjs/session-settings.js +76 -0
- package/dist/cjs/session-settings.js.map +1 -0
- package/dist/esm/acceptor.d.ts +30 -0
- package/dist/esm/acceptor.d.ts.map +1 -0
- package/dist/esm/acceptor.js +35 -0
- package/dist/esm/acceptor.js.map +1 -0
- package/dist/esm/application.d.ts +116 -0
- package/dist/esm/application.d.ts.map +1 -0
- package/dist/esm/application.js +58 -0
- package/dist/esm/application.js.map +1 -0
- package/dist/esm/data-dictionary.d.ts +49 -0
- package/dist/esm/data-dictionary.d.ts.map +1 -0
- package/dist/esm/data-dictionary.js +73 -0
- package/dist/esm/data-dictionary.js.map +1 -0
- package/dist/esm/engine.d.ts +114 -0
- package/dist/esm/engine.d.ts.map +1 -0
- package/dist/esm/engine.js +134 -0
- package/dist/esm/engine.js.map +1 -0
- package/dist/esm/enums.d.ts +22 -0
- package/dist/esm/enums.d.ts.map +1 -0
- package/dist/esm/enums.js +45 -0
- package/dist/esm/enums.js.map +1 -0
- package/dist/esm/generated/fields.d.ts +6123 -0
- package/dist/esm/generated/fields.d.ts.map +1 -0
- package/dist/esm/generated/fields.js +6125 -0
- package/dist/esm/generated/fields.js.map +1 -0
- package/dist/esm/index.d.ts +47 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +52 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/initiator.d.ts +30 -0
- package/dist/esm/initiator.d.ts.map +1 -0
- package/dist/esm/initiator.js +35 -0
- package/dist/esm/initiator.js.map +1 -0
- package/dist/esm/load-native.cjs +5 -0
- package/dist/esm/message.d.ts +114 -0
- package/dist/esm/message.d.ts.map +1 -0
- package/dist/esm/message.js +156 -0
- package/dist/esm/message.js.map +1 -0
- package/dist/esm/native.d.ts +163 -0
- package/dist/esm/native.d.ts.map +1 -0
- package/dist/esm/native.js +17 -0
- package/dist/esm/native.js.map +1 -0
- package/dist/esm/session-id.d.ts +40 -0
- package/dist/esm/session-id.d.ts.map +1 -0
- package/dist/esm/session-id.js +63 -0
- package/dist/esm/session-id.js.map +1 -0
- package/dist/esm/session-settings.d.ts +47 -0
- package/dist/esm/session-settings.d.ts.map +1 -0
- package/dist/esm/session-settings.js +72 -0
- package/dist/esm/session-settings.js.map +1 -0
- package/package.json +83 -4
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
- package/prebuilds/linux-x64/node.napi.musl.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/src/load-native.cjs +5 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#include "application_bridge.h"
|
|
2
|
+
|
|
3
|
+
#include <mutex>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <utility>
|
|
6
|
+
|
|
7
|
+
#include "message_wrap.h"
|
|
8
|
+
#include "session_id_wrap.h"
|
|
9
|
+
|
|
10
|
+
namespace napi_quickfix {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
// Map a CallType to the JS handler property name.
|
|
15
|
+
const char* HandlerName(ApplicationBridge::CallType type) {
|
|
16
|
+
switch (type) {
|
|
17
|
+
case ApplicationBridge::CallType::kOnCreate: return "onCreate";
|
|
18
|
+
case ApplicationBridge::CallType::kOnLogon: return "onLogon";
|
|
19
|
+
case ApplicationBridge::CallType::kOnLogout: return "onLogout";
|
|
20
|
+
case ApplicationBridge::CallType::kToAdmin: return "toAdmin";
|
|
21
|
+
case ApplicationBridge::CallType::kToApp: return "toApp";
|
|
22
|
+
case ApplicationBridge::CallType::kFromAdmin: return "fromAdmin";
|
|
23
|
+
case ApplicationBridge::CallType::kFromApp: return "fromApp";
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bool IsMessageBearing(ApplicationBridge::CallType type) {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case ApplicationBridge::CallType::kOnCreate:
|
|
31
|
+
case ApplicationBridge::CallType::kOnLogon:
|
|
32
|
+
case ApplicationBridge::CallType::kOnLogout:
|
|
33
|
+
return false;
|
|
34
|
+
default:
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Mutating (outbound) callbacks: the JS handler may edit the message.
|
|
40
|
+
bool IsMutating(ApplicationBridge::CallType type) {
|
|
41
|
+
return type == ApplicationBridge::CallType::kToAdmin ||
|
|
42
|
+
type == ApplicationBridge::CallType::kToApp;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} // namespace
|
|
46
|
+
|
|
47
|
+
ApplicationBridge::ApplicationBridge(Napi::Env env, Napi::Object handlers) {
|
|
48
|
+
if (!handlers.IsEmpty() && handlers.IsObject()) {
|
|
49
|
+
handlers_ = Napi::Persistent(handlers);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create the TSFN. No JS callback (the trampoline is the CallJs template
|
|
53
|
+
// param). Initial thread count of 1 represents the QuickFIX side; released on
|
|
54
|
+
// Release()/Abort(). Context = this. maxQueueSize 0 = unbounded.
|
|
55
|
+
tsfn_ = TSFN::New(
|
|
56
|
+
env,
|
|
57
|
+
"napi_quickfix::ApplicationBridge", // resource name
|
|
58
|
+
0, // unbounded queue
|
|
59
|
+
1, // initial thread count
|
|
60
|
+
this, // context
|
|
61
|
+
[](Napi::Env, void*, ApplicationBridge*) { /* finalizer: no-op */ });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ApplicationBridge::~ApplicationBridge() {
|
|
65
|
+
// Wake any straggling waiter so it can't block on a destroyed channel.
|
|
66
|
+
inactive_.store(true, std::memory_order_release);
|
|
67
|
+
WakeAllPending();
|
|
68
|
+
// Ensure the TSFN is released so it doesn't dangle. If already released this
|
|
69
|
+
// is a no-op-ish (Abort on an aborted tsfn returns an error we ignore).
|
|
70
|
+
if (!released_) {
|
|
71
|
+
tsfn_.Abort();
|
|
72
|
+
released_ = true;
|
|
73
|
+
}
|
|
74
|
+
if (!handlers_.IsEmpty()) {
|
|
75
|
+
handlers_.Reset();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void ApplicationBridge::Release() {
|
|
80
|
+
if (!released_) {
|
|
81
|
+
// Mark inactive BEFORE releasing so any QuickFIX thread that is about to
|
|
82
|
+
// dispatch sees it and bails out (pass-through) instead of racing the TSFN.
|
|
83
|
+
inactive_.store(true, std::memory_order_release);
|
|
84
|
+
WakeAllPending();
|
|
85
|
+
released_ = true;
|
|
86
|
+
tsfn_.Release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void ApplicationBridge::Abort() {
|
|
91
|
+
if (!released_) {
|
|
92
|
+
inactive_.store(true, std::memory_order_release);
|
|
93
|
+
WakeAllPending();
|
|
94
|
+
released_ = true;
|
|
95
|
+
tsfn_.Abort();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void ApplicationBridge::Deactivate() {
|
|
100
|
+
inactive_.store(true, std::memory_order_release);
|
|
101
|
+
// Release any network thread already blocked in CallSync so the subsequent
|
|
102
|
+
// engine stop() (which joins those threads) can complete without deadlocking
|
|
103
|
+
// on the frozen main loop.
|
|
104
|
+
WakeAllPending();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
std::uint64_t ApplicationBridge::RegisterChannel(
|
|
108
|
+
const std::shared_ptr<SyncChannel>& ch) {
|
|
109
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
110
|
+
std::uint64_t id = nextChannelId_++;
|
|
111
|
+
pending_[id] = ch;
|
|
112
|
+
return id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
void ApplicationBridge::UnregisterChannel(std::uint64_t id) {
|
|
116
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
117
|
+
pending_.erase(id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
void ApplicationBridge::WakeAllPending() {
|
|
121
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
122
|
+
for (auto& kv : pending_) {
|
|
123
|
+
auto& ch = kv.second;
|
|
124
|
+
{
|
|
125
|
+
std::lock_guard<std::mutex> chLock(ch->mtx);
|
|
126
|
+
if (!ch->done) {
|
|
127
|
+
ch->done = true; // pass-through (default Result: no mutate, no throw)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
ch->cv.notify_all();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
void ApplicationBridge::Ref(Napi::Env env) {
|
|
135
|
+
if (!released_) {
|
|
136
|
+
tsfn_.Ref(env);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void ApplicationBridge::Unref(Napi::Env env) {
|
|
141
|
+
if (!released_) {
|
|
142
|
+
tsfn_.Unref(env);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// FIX::Application overrides (invoked on QuickFIX threads).
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
void ApplicationBridge::FireAndForget(CallType type, const FIX::SessionID& id) {
|
|
151
|
+
// Bridge is releasing/aborting (possibly during finalize where the TSFN is
|
|
152
|
+
// being destroyed): don't touch the TSFN. Fire-and-forget just drops.
|
|
153
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
auto* data = new CallData();
|
|
157
|
+
data->type = type;
|
|
158
|
+
data->sessionID = id;
|
|
159
|
+
data->hasMessage = false;
|
|
160
|
+
// channel stays null: fire-and-forget.
|
|
161
|
+
napi_status status = tsfn_.NonBlockingCall(data);
|
|
162
|
+
if (status != napi_ok) {
|
|
163
|
+
delete data; // TSFN won't take ownership if the call failed.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
ApplicationBridge::Result ApplicationBridge::CallSync(CallType type,
|
|
168
|
+
const FIX::SessionID& id,
|
|
169
|
+
const std::string& raw) {
|
|
170
|
+
// Bridge is releasing/aborting: never call into the TSFN (its internals may be
|
|
171
|
+
// torn down during finalize -> calling would abort the process). Return an
|
|
172
|
+
// empty pass-through result so the QuickFIX thread proceeds without mutation
|
|
173
|
+
// or thrown exception.
|
|
174
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
175
|
+
Result r;
|
|
176
|
+
return r;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
auto channel = std::make_shared<SyncChannel>();
|
|
180
|
+
std::uint64_t channelId = RegisterChannel(channel);
|
|
181
|
+
|
|
182
|
+
auto* data = new CallData();
|
|
183
|
+
data->type = type;
|
|
184
|
+
data->sessionID = id;
|
|
185
|
+
data->rawMessage = raw;
|
|
186
|
+
data->hasMessage = true;
|
|
187
|
+
data->channel = channel; // shared: trampoline gets its own owning copy
|
|
188
|
+
|
|
189
|
+
napi_status status = tsfn_.BlockingCall(data);
|
|
190
|
+
if (status != napi_ok) {
|
|
191
|
+
// TSFN is closing/aborted: it won't take ownership and won't fulfill the
|
|
192
|
+
// channel. Clean up and return a pass-through result.
|
|
193
|
+
delete data;
|
|
194
|
+
UnregisterChannel(channelId);
|
|
195
|
+
Result r;
|
|
196
|
+
return r;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Wait for the trampoline (normal completion) or Abort/Deactivate (teardown)
|
|
200
|
+
// to signal the channel. Never blocks forever: WakeAllPending() sets done.
|
|
201
|
+
{
|
|
202
|
+
std::unique_lock<std::mutex> lock(channel->mtx);
|
|
203
|
+
channel->cv.wait(lock, [&channel] { return channel->done; });
|
|
204
|
+
}
|
|
205
|
+
UnregisterChannel(channelId);
|
|
206
|
+
std::lock_guard<std::mutex> lock(channel->mtx);
|
|
207
|
+
return channel->result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
void ApplicationBridge::onCreate(const FIX::SessionID& id) {
|
|
211
|
+
FireAndForget(CallType::kOnCreate, id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
void ApplicationBridge::onLogon(const FIX::SessionID& id) {
|
|
215
|
+
FireAndForget(CallType::kOnLogon, id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
void ApplicationBridge::onLogout(const FIX::SessionID& id) {
|
|
219
|
+
FireAndForget(CallType::kOnLogout, id);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
void ApplicationBridge::toAdmin(FIX::Message& message,
|
|
223
|
+
const FIX::SessionID& id) {
|
|
224
|
+
Result r = CallSync(CallType::kToAdmin, id, message.toString());
|
|
225
|
+
if (r.mutated) {
|
|
226
|
+
message.setString(r.editedMessage, false);
|
|
227
|
+
}
|
|
228
|
+
// toAdmin doesn't throw in the FIX interface; ignore r.threw here.
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
void ApplicationBridge::toApp(FIX::Message& message, const FIX::SessionID& id)
|
|
232
|
+
EXCEPT(FIX::DoNotSend) {
|
|
233
|
+
Result r = CallSync(CallType::kToApp, id, message.toString());
|
|
234
|
+
if (r.threw) {
|
|
235
|
+
// toApp may throw DoNotSend.
|
|
236
|
+
throw FIX::DoNotSend();
|
|
237
|
+
}
|
|
238
|
+
if (r.mutated) {
|
|
239
|
+
message.setString(r.editedMessage, false);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
void ApplicationBridge::fromAdmin(const FIX::Message& message,
|
|
244
|
+
const FIX::SessionID& id)
|
|
245
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
246
|
+
FIX::RejectLogon) {
|
|
247
|
+
Result r = CallSync(CallType::kFromAdmin, id, message.toString());
|
|
248
|
+
if (r.threw) {
|
|
249
|
+
if (r.errorName == "RejectLogon") throw FIX::RejectLogon(r.errorMessage);
|
|
250
|
+
if (r.errorName == "IncorrectDataFormat") throw FIX::IncorrectDataFormat(0);
|
|
251
|
+
if (r.errorName == "IncorrectTagValue") throw FIX::IncorrectTagValue(0);
|
|
252
|
+
if (r.errorName == "FieldNotFound") throw FIX::FieldNotFound(0);
|
|
253
|
+
// Default: reject the logon so the peer sees a definitive response.
|
|
254
|
+
throw FIX::RejectLogon(r.errorMessage);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void ApplicationBridge::fromApp(const FIX::Message& message,
|
|
259
|
+
const FIX::SessionID& id)
|
|
260
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
261
|
+
FIX::UnsupportedMessageType) {
|
|
262
|
+
Result r = CallSync(CallType::kFromApp, id, message.toString());
|
|
263
|
+
if (r.threw) {
|
|
264
|
+
if (r.errorName == "UnsupportedMessageType")
|
|
265
|
+
throw FIX::UnsupportedMessageType();
|
|
266
|
+
if (r.errorName == "IncorrectDataFormat") throw FIX::IncorrectDataFormat(0);
|
|
267
|
+
if (r.errorName == "IncorrectTagValue") throw FIX::IncorrectTagValue(0);
|
|
268
|
+
if (r.errorName == "FieldNotFound") throw FIX::FieldNotFound(0);
|
|
269
|
+
throw FIX::UnsupportedMessageType();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// JS-thread trampoline. Runs on the Node event-loop thread.
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
void ApplicationBridge::CallJs(Napi::Env env, Napi::Function /*jsCallback*/,
|
|
278
|
+
ApplicationBridge* context, CallData* data) {
|
|
279
|
+
// RAII: always delete the CallData and, for sync calls, ALWAYS signal the
|
|
280
|
+
// channel even on early return or a thrown C++ exception in this trampoline.
|
|
281
|
+
// The signal is a no-op if Abort/Deactivate already woke the waiter (done set)
|
|
282
|
+
// — the waiter has already returned pass-through and discarded the channel.
|
|
283
|
+
struct Guard {
|
|
284
|
+
CallData* data;
|
|
285
|
+
Result result;
|
|
286
|
+
~Guard() {
|
|
287
|
+
if (data->channel) {
|
|
288
|
+
auto& ch = *data->channel;
|
|
289
|
+
{
|
|
290
|
+
std::lock_guard<std::mutex> lock(ch.mtx);
|
|
291
|
+
if (!ch.done) {
|
|
292
|
+
ch.result = std::move(result);
|
|
293
|
+
ch.done = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
ch.cv.notify_all();
|
|
297
|
+
}
|
|
298
|
+
delete data;
|
|
299
|
+
}
|
|
300
|
+
} guard{data, {}};
|
|
301
|
+
|
|
302
|
+
// env may be null during environment teardown; nothing safe to do.
|
|
303
|
+
if (env == nullptr || context == nullptr) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const bool isSync = (data->channel != nullptr);
|
|
308
|
+
|
|
309
|
+
Napi::HandleScope scope(env);
|
|
310
|
+
|
|
311
|
+
// Resolve the handler function, if the user provided one.
|
|
312
|
+
Napi::Function handler;
|
|
313
|
+
if (!context->handlers_.IsEmpty()) {
|
|
314
|
+
Napi::Object handlers = context->handlers_.Value();
|
|
315
|
+
Napi::Value h = handlers.Get(HandlerName(data->type));
|
|
316
|
+
if (h.IsFunction()) {
|
|
317
|
+
handler = h.As<Napi::Function>();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Build the JS arguments: (message?, sessionID).
|
|
322
|
+
Napi::Object sessionIdObj = SessionIDWrap::NewInstance(env, data->sessionID);
|
|
323
|
+
|
|
324
|
+
Napi::Object msgObj;
|
|
325
|
+
bool haveMsg = false;
|
|
326
|
+
if (data->hasMessage) {
|
|
327
|
+
FIX::Message msg;
|
|
328
|
+
try {
|
|
329
|
+
msg.setString(data->rawMessage, false);
|
|
330
|
+
} catch (...) {
|
|
331
|
+
// Leave msg default-constructed if parse fails; still hand it to JS.
|
|
332
|
+
}
|
|
333
|
+
msgObj = MessageWrap::NewInstance(env, msg);
|
|
334
|
+
haveMsg = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (handler.IsEmpty()) {
|
|
338
|
+
// No handler for this callback: pass-through (no mutation, no throw).
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
if (haveMsg) {
|
|
344
|
+
handler.Call({msgObj, sessionIdObj});
|
|
345
|
+
} else {
|
|
346
|
+
handler.Call({sessionIdObj});
|
|
347
|
+
}
|
|
348
|
+
} catch (const Napi::Error& e) {
|
|
349
|
+
if (isSync) {
|
|
350
|
+
guard.result.threw = true;
|
|
351
|
+
// Read a `.fixError` / name off the JS error to pick the FIX exception.
|
|
352
|
+
Napi::Value nameVal;
|
|
353
|
+
try {
|
|
354
|
+
nameVal = e.Value().As<Napi::Object>().Get("fixError");
|
|
355
|
+
} catch (...) {
|
|
356
|
+
}
|
|
357
|
+
if (nameVal.IsString()) {
|
|
358
|
+
guard.result.errorName = nameVal.As<Napi::String>().Utf8Value();
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
guard.result.errorMessage = e.Message();
|
|
362
|
+
} catch (...) {
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// For fire-and-forget, the JS error is swallowed here (there's no FIX-thread
|
|
366
|
+
// waiting). Reported best-effort by leaving it; QuickFIX doesn't care.
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// For mutating sync callbacks, read back the (possibly edited) message.
|
|
370
|
+
if (isSync && IsMutating(data->type) && haveMsg && !guard.result.threw) {
|
|
371
|
+
try {
|
|
372
|
+
MessageWrap* w = Napi::ObjectWrap<MessageWrap>::Unwrap(msgObj);
|
|
373
|
+
guard.result.editedMessage = w->Message().toString();
|
|
374
|
+
guard.result.mutated = true;
|
|
375
|
+
} catch (...) {
|
|
376
|
+
guard.result.mutated = false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// guard destructor signals the channel (for sync) and deletes data.
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
} // namespace napi_quickfix
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_APPLICATION_BRIDGE_H
|
|
2
|
+
#define NAPI_QUICKFIX_APPLICATION_BRIDGE_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include <atomic>
|
|
7
|
+
#include <condition_variable>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <mutex>
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <unordered_map>
|
|
13
|
+
|
|
14
|
+
#include "quickfix/Application.h"
|
|
15
|
+
#include "quickfix/Message.h"
|
|
16
|
+
#include "quickfix/SessionID.h"
|
|
17
|
+
|
|
18
|
+
namespace napi_quickfix {
|
|
19
|
+
|
|
20
|
+
// Bridges QuickFIX's FIX::Application callbacks (invoked on QuickFIX's own
|
|
21
|
+
// network threads) to a set of JS handler functions, via a SINGLE
|
|
22
|
+
// TypedThreadSafeFunction dispatcher.
|
|
23
|
+
//
|
|
24
|
+
// Fire-and-forget callbacks (onCreate/onLogon/onLogout) use NonBlockingCall and
|
|
25
|
+
// never wait. Synchronous mutate/throw callbacks (toAdmin/toApp/fromAdmin/
|
|
26
|
+
// fromApp) use BlockingCall + a shared SyncChannel (mutex/condition_variable) so
|
|
27
|
+
// the QuickFIX thread observes any message mutation or thrown FIX exception
|
|
28
|
+
// before returning. Crucially, the wait can be released not only by the JS
|
|
29
|
+
// trampoline but also by Deactivate()/Abort() during teardown, so a network
|
|
30
|
+
// thread never blocks forever when the main event loop is frozen (e.g. inside a
|
|
31
|
+
// finalize/cleanup hook). The engine wraps run start/stop/sendToTarget on libuv
|
|
32
|
+
// worker threads (AsyncWorker) so the main loop stays free to service these
|
|
33
|
+
// BlockingCall trampolines — this is what breaks the original deadlock.
|
|
34
|
+
class ApplicationBridge : public FIX::Application {
|
|
35
|
+
public:
|
|
36
|
+
enum class CallType {
|
|
37
|
+
kOnCreate,
|
|
38
|
+
kOnLogon,
|
|
39
|
+
kOnLogout,
|
|
40
|
+
kToAdmin,
|
|
41
|
+
kToApp,
|
|
42
|
+
kFromAdmin,
|
|
43
|
+
kFromApp,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Result returned from the JS trampoline for synchronous calls.
|
|
47
|
+
struct Result {
|
|
48
|
+
// For mutating callbacks (toAdmin/toApp), the (possibly edited) message.
|
|
49
|
+
std::string editedMessage;
|
|
50
|
+
bool mutated = false;
|
|
51
|
+
// If the JS handler threw, the FIX exception class name to re-throw
|
|
52
|
+
// (e.g. "DoNotSend", "RejectLogon", "UnsupportedMessageType") and message.
|
|
53
|
+
bool threw = false;
|
|
54
|
+
std::string errorName;
|
|
55
|
+
std::string errorMessage;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Reply channel for a synchronous call. Heap-allocated and shared (shared_ptr)
|
|
59
|
+
// between the waiting QuickFIX thread and the JS trampoline so neither can
|
|
60
|
+
// dangle. Guarded by a mutex + condition_variable so the waiter can be woken
|
|
61
|
+
// either by the trampoline (normal completion) or by Abort()/Deactivate()
|
|
62
|
+
// (teardown), instead of blocking forever on a future when the main loop is
|
|
63
|
+
// frozen inside a cleanup hook.
|
|
64
|
+
struct SyncChannel {
|
|
65
|
+
std::mutex mtx;
|
|
66
|
+
std::condition_variable cv;
|
|
67
|
+
bool done = false;
|
|
68
|
+
Result result;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
struct CallData {
|
|
72
|
+
CallType type;
|
|
73
|
+
FIX::SessionID sessionID; // copied by value — safe across threads
|
|
74
|
+
std::string rawMessage; // serialized message for message-bearing calls
|
|
75
|
+
bool hasMessage = false;
|
|
76
|
+
// For synchronous calls only: shared reply channel (null for fire-and-forget).
|
|
77
|
+
std::shared_ptr<SyncChannel> channel;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// The JS-thread trampoline. Runs on the Node event-loop thread. Declared
|
|
81
|
+
// before the TSFN alias because it is used as a template argument.
|
|
82
|
+
static void CallJs(Napi::Env env, Napi::Function jsCallback,
|
|
83
|
+
ApplicationBridge* context, CallData* data);
|
|
84
|
+
|
|
85
|
+
using TSFN = Napi::TypedThreadSafeFunction<ApplicationBridge, CallData,
|
|
86
|
+
ApplicationBridge::CallJs>;
|
|
87
|
+
|
|
88
|
+
// Construct on the JS thread. `handlers` is the user's handlers object (may be
|
|
89
|
+
// empty/undefined). `env` is captured for control (Ref/Unref).
|
|
90
|
+
ApplicationBridge(Napi::Env env, Napi::Object handlers);
|
|
91
|
+
~ApplicationBridge() override;
|
|
92
|
+
|
|
93
|
+
// TSFN lifecycle. Acquire is a no-op placeholder (the initial thread count is
|
|
94
|
+
// 1 from New); Release drains, Abort force-stops. Ref/Unref control whether
|
|
95
|
+
// the TSFN keeps the event loop alive.
|
|
96
|
+
void Release();
|
|
97
|
+
void Abort();
|
|
98
|
+
// Mark the bridge inactive WITHOUT touching the TSFN. Callbacks arriving on
|
|
99
|
+
// QuickFIX threads will pass through immediately. Call this on the MAIN thread
|
|
100
|
+
// BEFORE stopping the engine during finalize, so the subsequent engine stop()
|
|
101
|
+
// (which joins the network threads) can't have a callback race into a TSFN
|
|
102
|
+
// that is about to be aborted.
|
|
103
|
+
void Deactivate();
|
|
104
|
+
void Ref(Napi::Env env);
|
|
105
|
+
void Unref(Napi::Env env);
|
|
106
|
+
|
|
107
|
+
// FIX::Application overrides.
|
|
108
|
+
void onCreate(const FIX::SessionID&) override;
|
|
109
|
+
void onLogon(const FIX::SessionID&) override;
|
|
110
|
+
void onLogout(const FIX::SessionID&) override;
|
|
111
|
+
void toAdmin(FIX::Message&, const FIX::SessionID&) override;
|
|
112
|
+
void toApp(FIX::Message&, const FIX::SessionID&) EXCEPT(FIX::DoNotSend) override;
|
|
113
|
+
void fromAdmin(const FIX::Message&, const FIX::SessionID&)
|
|
114
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat,
|
|
115
|
+
FIX::IncorrectTagValue, FIX::RejectLogon) override;
|
|
116
|
+
void fromApp(const FIX::Message&, const FIX::SessionID&)
|
|
117
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat,
|
|
118
|
+
FIX::IncorrectTagValue, FIX::UnsupportedMessageType) override;
|
|
119
|
+
|
|
120
|
+
private:
|
|
121
|
+
void FireAndForget(CallType type, const FIX::SessionID& id);
|
|
122
|
+
// Runs a synchronous call and returns the Result (throwing FIX exception is
|
|
123
|
+
// handled by the caller-side override, which inspects Result).
|
|
124
|
+
Result CallSync(CallType type, const FIX::SessionID& id,
|
|
125
|
+
const std::string& raw);
|
|
126
|
+
|
|
127
|
+
Napi::ObjectReference handlers_;
|
|
128
|
+
TSFN tsfn_;
|
|
129
|
+
bool released_ = false;
|
|
130
|
+
// Set true by Abort()/Release() (which may run on the MAIN thread during
|
|
131
|
+
// finalize) and read by the FIX::Application overrides (which run on QuickFIX
|
|
132
|
+
// network threads). Once true, the overrides must NOT touch the TSFN — during
|
|
133
|
+
// environment teardown the TSFN's internals may already be destroyed, and
|
|
134
|
+
// calling into it aborts the process. Atomic so the cross-thread read is safe.
|
|
135
|
+
std::atomic<bool> inactive_{false};
|
|
136
|
+
|
|
137
|
+
// Wake all in-flight synchronous callers (used by Deactivate/Abort so a
|
|
138
|
+
// network thread blocked in CallSync doesn't wait forever when the main loop
|
|
139
|
+
// is frozen inside a cleanup hook). Registers each SyncChannel while pending.
|
|
140
|
+
std::mutex pendingMtx_;
|
|
141
|
+
std::unordered_map<std::uint64_t, std::shared_ptr<SyncChannel>> pending_;
|
|
142
|
+
std::uint64_t nextChannelId_ = 0;
|
|
143
|
+
|
|
144
|
+
std::uint64_t RegisterChannel(const std::shared_ptr<SyncChannel>& ch);
|
|
145
|
+
void UnregisterChannel(std::uint64_t id);
|
|
146
|
+
void WakeAllPending();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
} // namespace napi_quickfix
|
|
150
|
+
|
|
151
|
+
#endif // NAPI_QUICKFIX_APPLICATION_BRIDGE_H
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#include "data_dictionary_wrap.h"
|
|
2
|
+
|
|
3
|
+
#include <sstream>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "errors.h"
|
|
7
|
+
#include "message_wrap.h"
|
|
8
|
+
|
|
9
|
+
namespace napi_quickfix {
|
|
10
|
+
|
|
11
|
+
Napi::FunctionReference DataDictionaryWrap::constructor_;
|
|
12
|
+
|
|
13
|
+
Napi::Object DataDictionaryWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
14
|
+
Napi::Function func = DefineClass(
|
|
15
|
+
env, "DataDictionary",
|
|
16
|
+
{
|
|
17
|
+
StaticMethod("fromFile", &DataDictionaryWrap::FromFile),
|
|
18
|
+
StaticMethod("fromString", &DataDictionaryWrap::FromString),
|
|
19
|
+
InstanceMethod("validate", &DataDictionaryWrap::Validate),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
constructor_ = Napi::Persistent(func);
|
|
23
|
+
constructor_.SuppressDestruct();
|
|
24
|
+
|
|
25
|
+
exports.Set("DataDictionaryWrap", func);
|
|
26
|
+
return exports;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Constructor is internal: (kind: "string"|"file", payload: string).
|
|
30
|
+
DataDictionaryWrap::DataDictionaryWrap(const Napi::CallbackInfo& info)
|
|
31
|
+
: Napi::ObjectWrap<DataDictionaryWrap>(info) {
|
|
32
|
+
Napi::Env env = info.Env();
|
|
33
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
34
|
+
throw Napi::TypeError::New(
|
|
35
|
+
env,
|
|
36
|
+
"DataDictionary cannot be constructed directly; use "
|
|
37
|
+
"DataDictionary.fromFile() or DataDictionary.fromString()");
|
|
38
|
+
}
|
|
39
|
+
std::string kind = info[0].As<Napi::String>().Utf8Value();
|
|
40
|
+
std::string payload = info[1].As<Napi::String>().Utf8Value();
|
|
41
|
+
|
|
42
|
+
NQ_TRY(env) {
|
|
43
|
+
if (kind == "string") {
|
|
44
|
+
std::istringstream stream(payload);
|
|
45
|
+
dict_.readFromStream(stream);
|
|
46
|
+
} else { // "file"
|
|
47
|
+
dict_.readFromURL(payload);
|
|
48
|
+
}
|
|
49
|
+
} NQ_CATCH(env)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Napi::Value DataDictionaryWrap::FromFile(const Napi::CallbackInfo& info) {
|
|
53
|
+
Napi::Env env = info.Env();
|
|
54
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
55
|
+
throw Napi::TypeError::New(env, "fromFile(path: string)");
|
|
56
|
+
}
|
|
57
|
+
return constructor_.New(
|
|
58
|
+
{Napi::String::New(env, "file"), info[0].As<Napi::String>()});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Napi::Value DataDictionaryWrap::FromString(const Napi::CallbackInfo& info) {
|
|
62
|
+
Napi::Env env = info.Env();
|
|
63
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
64
|
+
throw Napi::TypeError::New(env, "fromString(xml: string)");
|
|
65
|
+
}
|
|
66
|
+
return constructor_.New(
|
|
67
|
+
{Napi::String::New(env, "string"), info[0].As<Napi::String>()});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Napi::Value DataDictionaryWrap::Validate(const Napi::CallbackInfo& info) {
|
|
71
|
+
Napi::Env env = info.Env();
|
|
72
|
+
MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
|
|
73
|
+
NQ_TRY(env) {
|
|
74
|
+
dict_.validate(msg->Message());
|
|
75
|
+
} NQ_CATCH(env)
|
|
76
|
+
return env.Undefined();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
} // namespace napi_quickfix
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
2
|
+
#define NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include "quickfix/DataDictionary.h"
|
|
7
|
+
|
|
8
|
+
namespace napi_quickfix {
|
|
9
|
+
|
|
10
|
+
// Napi::ObjectWrap over FIX::DataDictionary.
|
|
11
|
+
//
|
|
12
|
+
// DataDictionary.fromFile(path): DataDictionary
|
|
13
|
+
// DataDictionary.fromString(xml): DataDictionary
|
|
14
|
+
// dd.validate(message: Message): void (throws QuickFixError if invalid)
|
|
15
|
+
class DataDictionaryWrap : public Napi::ObjectWrap<DataDictionaryWrap> {
|
|
16
|
+
public:
|
|
17
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
18
|
+
|
|
19
|
+
explicit DataDictionaryWrap(const Napi::CallbackInfo& info);
|
|
20
|
+
|
|
21
|
+
FIX::DataDictionary& Dictionary() { return dict_; }
|
|
22
|
+
const FIX::DataDictionary& Dictionary() const { return dict_; }
|
|
23
|
+
|
|
24
|
+
private:
|
|
25
|
+
static Napi::FunctionReference constructor_;
|
|
26
|
+
|
|
27
|
+
static Napi::Value FromFile(const Napi::CallbackInfo& info);
|
|
28
|
+
static Napi::Value FromString(const Napi::CallbackInfo& info);
|
|
29
|
+
|
|
30
|
+
Napi::Value Validate(const Napi::CallbackInfo& info);
|
|
31
|
+
|
|
32
|
+
FIX::DataDictionary dict_;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
} // namespace napi_quickfix
|
|
36
|
+
|
|
37
|
+
#endif // NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_ENGINE_COMMON_H
|
|
2
|
+
#define NAPI_QUICKFIX_ENGINE_COMMON_H
|
|
3
|
+
|
|
4
|
+
#include "quickfix/Log.h"
|
|
5
|
+
|
|
6
|
+
namespace napi_quickfix {
|
|
7
|
+
|
|
8
|
+
// A LogFactory that produces no-op NullLog instances, for log:'none'. QuickFIX
|
|
9
|
+
// ships NullLog but no public NullLogFactory, so we provide one.
|
|
10
|
+
class NullLogFactory : public FIX::LogFactory {
|
|
11
|
+
public:
|
|
12
|
+
FIX::Log* create() override { return new FIX::NullLog(); }
|
|
13
|
+
FIX::Log* create(const FIX::SessionID&) override { return new FIX::NullLog(); }
|
|
14
|
+
void destroy(FIX::Log* log) override { delete log; }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
} // namespace napi_quickfix
|
|
18
|
+
|
|
19
|
+
#endif // NAPI_QUICKFIX_ENGINE_COMMON_H
|