@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.
- package/CMakeLists.txt +135 -0
- package/LICENSE +21 -0
- package/README.md +638 -2
- package/THIRD_PARTY_LICENSES +48 -0
- package/cpp/acceptor_wrap.cpp +468 -0
- package/cpp/acceptor_wrap.h +116 -0
- package/cpp/addon.cpp +87 -0
- package/cpp/application_bridge.cpp +466 -0
- package/cpp/application_bridge.h +204 -0
- package/cpp/data_dictionary_wrap.cpp +165 -0
- package/cpp/data_dictionary_wrap.h +59 -0
- package/cpp/engine_common.cpp +5 -0
- package/cpp/engine_common.h +127 -0
- package/cpp/engine_workers.h +167 -0
- package/cpp/errors.h +142 -0
- package/cpp/field_map_ops.h +492 -0
- package/cpp/group_wrap.cpp +221 -0
- package/cpp/group_wrap.h +76 -0
- package/cpp/initiator_wrap.cpp +492 -0
- package/cpp/initiator_wrap.h +104 -0
- package/cpp/message_wrap.cpp +288 -0
- package/cpp/message_wrap.h +95 -0
- package/cpp/session_id_wrap.cpp +91 -0
- package/cpp/session_id_wrap.h +46 -0
- package/cpp/session_op_gate.h +201 -0
- package/cpp/session_settings_wrap.cpp +98 -0
- package/cpp/session_settings_wrap.h +40 -0
- package/cpp/session_static.cpp +220 -0
- package/cpp/session_wrap.cpp +438 -0
- package/cpp/session_wrap.h +144 -0
- package/dist/cjs/acceptor.d.ts +29 -0
- package/dist/cjs/acceptor.js +38 -0
- package/dist/cjs/application.d.ts +115 -0
- package/dist/cjs/application.js +62 -0
- package/dist/cjs/data-dictionary.d.ts +67 -0
- package/dist/cjs/data-dictionary.js +105 -0
- package/dist/cjs/engine.d.ts +152 -0
- package/dist/cjs/engine.js +183 -0
- package/dist/cjs/enums.d.ts +56 -0
- package/dist/cjs/enums.js +44 -0
- package/dist/cjs/field-value.d.ts +12 -0
- package/dist/cjs/field-value.js +40 -0
- package/dist/cjs/generated/fields.d.ts +6122 -0
- package/dist/cjs/generated/fields.js +6127 -0
- package/dist/cjs/generated/values.d.ts +8548 -0
- package/dist/cjs/generated/values.js +8580 -0
- package/dist/cjs/group.d.ts +126 -0
- package/dist/cjs/group.js +185 -0
- package/dist/cjs/index.d.ts +68 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/initiator.d.ts +29 -0
- package/dist/cjs/initiator.js +38 -0
- package/dist/cjs/load-native.cjs +5 -0
- package/dist/cjs/message.d.ts +238 -0
- package/dist/cjs/message.js +299 -0
- package/dist/cjs/native.d.ts +325 -0
- package/dist/cjs/native.js +22 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/session-id.d.ts +51 -0
- package/dist/cjs/session-id.js +87 -0
- package/dist/cjs/session-settings.d.ts +46 -0
- package/dist/cjs/session-settings.js +75 -0
- package/dist/cjs/session.d.ts +213 -0
- package/dist/cjs/session.js +337 -0
- package/dist/esm/acceptor.d.ts +29 -0
- package/dist/esm/acceptor.js +34 -0
- package/dist/esm/application.d.ts +115 -0
- package/dist/esm/application.js +57 -0
- package/dist/esm/data-dictionary.d.ts +67 -0
- package/dist/esm/data-dictionary.js +101 -0
- package/dist/esm/engine.d.ts +152 -0
- package/dist/esm/engine.js +179 -0
- package/dist/esm/enums.d.ts +56 -0
- package/dist/esm/enums.js +35 -0
- package/dist/esm/field-value.d.ts +12 -0
- package/dist/esm/field-value.js +37 -0
- package/dist/esm/generated/fields.d.ts +6122 -0
- package/dist/esm/generated/fields.js +6124 -0
- package/dist/esm/generated/values.d.ts +8548 -0
- package/dist/esm/generated/values.js +8564 -0
- package/dist/esm/group.d.ts +126 -0
- package/dist/esm/group.js +181 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.js +43 -0
- package/dist/esm/initiator.d.ts +29 -0
- package/dist/esm/initiator.js +34 -0
- package/dist/esm/load-native.cjs +5 -0
- package/dist/esm/message.d.ts +238 -0
- package/dist/esm/message.js +293 -0
- package/dist/esm/native.d.ts +325 -0
- package/dist/esm/native.js +16 -0
- package/dist/esm/session-id.d.ts +51 -0
- package/dist/esm/session-id.js +82 -0
- package/dist/esm/session-settings.d.ts +46 -0
- package/dist/esm/session-settings.js +71 -0
- package/dist/esm/session.d.ts +213 -0
- package/dist/esm/session.js +329 -0
- package/package.json +95 -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
package/cpp/addon.cpp
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Module init for @homaiohq/napi-quickfix: registers all wrappers and
|
|
2
|
+
// module-level functions. FIX constants (field tags, value groups) are not
|
|
3
|
+
// exported here: they are generated on the TS side from the QuickFIX headers
|
|
4
|
+
// (scripts/gen-fields.mjs, scripts/gen-values.mjs).
|
|
5
|
+
|
|
6
|
+
#include <napi.h>
|
|
7
|
+
|
|
8
|
+
#include <exception>
|
|
9
|
+
#include <string>
|
|
10
|
+
|
|
11
|
+
#include "quickfix/Exceptions.h"
|
|
12
|
+
#include "quickfix/Field.h"
|
|
13
|
+
#include "quickfix/FieldNumbers.h"
|
|
14
|
+
#include "quickfix/Message.h"
|
|
15
|
+
|
|
16
|
+
#include "acceptor_wrap.h"
|
|
17
|
+
#include "data_dictionary_wrap.h"
|
|
18
|
+
#include "group_wrap.h"
|
|
19
|
+
#include "initiator_wrap.h"
|
|
20
|
+
#include "message_wrap.h"
|
|
21
|
+
#include "session_id_wrap.h"
|
|
22
|
+
#include "session_settings_wrap.h"
|
|
23
|
+
#include "session_wrap.h"
|
|
24
|
+
|
|
25
|
+
namespace napi_quickfix {
|
|
26
|
+
|
|
27
|
+
// Defined in session_static.cpp.
|
|
28
|
+
void RegisterSessionStatic(Napi::Env env, Napi::Object exports);
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
// Keep in sync with package.json "version".
|
|
33
|
+
constexpr const char* kAddonVersion = "0.2.0";
|
|
34
|
+
|
|
35
|
+
Napi::Value Version(const Napi::CallbackInfo& info) {
|
|
36
|
+
return Napi::String::New(info.Env(), kAddonVersion);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Phase-1 regression smoke: prove quickfix parsing still links & works.
|
|
40
|
+
Napi::Value QuickfixParseMsgType(const Napi::CallbackInfo& info) {
|
|
41
|
+
Napi::Env env = info.Env();
|
|
42
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
43
|
+
throw Napi::TypeError::New(
|
|
44
|
+
env, "quickfixParseMsgType(raw: string): expected a string argument");
|
|
45
|
+
}
|
|
46
|
+
const std::string raw = info[0].As<Napi::String>().Utf8Value();
|
|
47
|
+
try {
|
|
48
|
+
FIX::Message message(raw, /*validate=*/false);
|
|
49
|
+
const std::string& msgType = message.getHeader().getField(FIX::FIELD::MsgType);
|
|
50
|
+
return Napi::String::New(env, msgType);
|
|
51
|
+
} catch (const FIX::Exception& e) {
|
|
52
|
+
Napi::Error err = Napi::Error::New(env, e.what());
|
|
53
|
+
err.Set("fixError", Napi::String::New(env, "InvalidMessage"));
|
|
54
|
+
throw err;
|
|
55
|
+
} catch (const std::exception& e) {
|
|
56
|
+
throw Napi::Error::New(env, e.what());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
61
|
+
exports.Set("version", Napi::Function::New(env, Version, "version"));
|
|
62
|
+
exports.Set("quickfixParseMsgType",
|
|
63
|
+
Napi::Function::New(env, QuickfixParseMsgType, "quickfixParseMsgType"));
|
|
64
|
+
|
|
65
|
+
MessageWrap::Init(env, exports);
|
|
66
|
+
GroupWrap::Init(env, exports);
|
|
67
|
+
SessionIDWrap::Init(env, exports);
|
|
68
|
+
SessionSettingsWrap::Init(env, exports);
|
|
69
|
+
DataDictionaryWrap::Init(env, exports);
|
|
70
|
+
SessionWrap::Init(env, exports);
|
|
71
|
+
InitiatorWrap::Init(env, exports);
|
|
72
|
+
AcceptorWrap::Init(env, exports);
|
|
73
|
+
|
|
74
|
+
RegisterSessionStatic(env, exports);
|
|
75
|
+
|
|
76
|
+
return exports;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
} // namespace
|
|
80
|
+
|
|
81
|
+
} // namespace napi_quickfix
|
|
82
|
+
|
|
83
|
+
static Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
|
|
84
|
+
return napi_quickfix::Init(env, exports);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
NODE_API_MODULE(napi_quickfix, InitModule)
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#include "application_bridge.h"
|
|
2
|
+
|
|
3
|
+
#include <memory>
|
|
4
|
+
#include <mutex>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <utility>
|
|
7
|
+
|
|
8
|
+
#include "field_map_ops.h"
|
|
9
|
+
#include "message_wrap.h"
|
|
10
|
+
#include "quickfix/DataDictionaryProvider.h"
|
|
11
|
+
#include "quickfix/Session.h"
|
|
12
|
+
#include "session_id_wrap.h"
|
|
13
|
+
|
|
14
|
+
namespace napi_quickfix {
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
// Map a CallType to the JS handler property name.
|
|
19
|
+
const char* HandlerName(ApplicationBridge::CallType type) {
|
|
20
|
+
switch (type) {
|
|
21
|
+
case ApplicationBridge::CallType::kOnCreate: return "onCreate";
|
|
22
|
+
case ApplicationBridge::CallType::kOnLogon: return "onLogon";
|
|
23
|
+
case ApplicationBridge::CallType::kOnLogout: return "onLogout";
|
|
24
|
+
case ApplicationBridge::CallType::kToAdmin: return "toAdmin";
|
|
25
|
+
case ApplicationBridge::CallType::kToApp: return "toApp";
|
|
26
|
+
case ApplicationBridge::CallType::kFromAdmin: return "fromAdmin";
|
|
27
|
+
case ApplicationBridge::CallType::kFromApp: return "fromApp";
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bool IsMessageBearing(ApplicationBridge::CallType type) {
|
|
33
|
+
switch (type) {
|
|
34
|
+
case ApplicationBridge::CallType::kOnCreate:
|
|
35
|
+
case ApplicationBridge::CallType::kOnLogon:
|
|
36
|
+
case ApplicationBridge::CallType::kOnLogout:
|
|
37
|
+
return false;
|
|
38
|
+
default:
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Mutating (outbound) callbacks: the JS handler may edit the message.
|
|
44
|
+
bool IsMutating(ApplicationBridge::CallType type) {
|
|
45
|
+
return type == ApplicationBridge::CallType::kToAdmin ||
|
|
46
|
+
type == ApplicationBridge::CallType::kToApp;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} // namespace
|
|
50
|
+
|
|
51
|
+
ApplicationBridge::ApplicationBridge(Napi::Env env, Napi::Object handlers) {
|
|
52
|
+
if (!handlers.IsEmpty() && handlers.IsObject()) {
|
|
53
|
+
handlers_ = Napi::Persistent(handlers);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create the TSFN. No JS callback (the trampoline is the CallJs template
|
|
57
|
+
// param). Initial thread count of 1 represents the QuickFIX side; released on
|
|
58
|
+
// Release()/Abort(). Context = this. maxQueueSize 0 = unbounded.
|
|
59
|
+
tsfn_ = TSFN::New(
|
|
60
|
+
env,
|
|
61
|
+
"napi_quickfix::ApplicationBridge", // resource name
|
|
62
|
+
0, // unbounded queue
|
|
63
|
+
1, // initial thread count
|
|
64
|
+
this, // context
|
|
65
|
+
[](Napi::Env, void*, ApplicationBridge*) { /* finalizer: no-op */ });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ApplicationBridge::~ApplicationBridge() {
|
|
69
|
+
// Wake any straggling waiter so it can't block on a destroyed channel.
|
|
70
|
+
inactive_.store(true, std::memory_order_release);
|
|
71
|
+
WakeAllPending();
|
|
72
|
+
// Ensure the TSFN is released so it doesn't dangle. If already released this
|
|
73
|
+
// is a no-op-ish (Abort on an aborted tsfn returns an error we ignore).
|
|
74
|
+
if (!released_) {
|
|
75
|
+
tsfn_.Abort();
|
|
76
|
+
released_ = true;
|
|
77
|
+
}
|
|
78
|
+
if (!handlers_.IsEmpty()) {
|
|
79
|
+
handlers_.Reset();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void ApplicationBridge::Release() {
|
|
84
|
+
if (!released_) {
|
|
85
|
+
// Mark inactive BEFORE releasing so any QuickFIX thread that is about to
|
|
86
|
+
// dispatch sees it and bails out (pass-through) instead of racing the TSFN.
|
|
87
|
+
inactive_.store(true, std::memory_order_release);
|
|
88
|
+
WakeAllPending();
|
|
89
|
+
released_ = true;
|
|
90
|
+
tsfn_.Release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void ApplicationBridge::Abort() {
|
|
95
|
+
if (!released_) {
|
|
96
|
+
inactive_.store(true, std::memory_order_release);
|
|
97
|
+
WakeAllPending();
|
|
98
|
+
released_ = true;
|
|
99
|
+
tsfn_.Abort();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bool ApplicationBridge::RunAfterQueued(Napi::Env env,
|
|
104
|
+
std::function<void(Napi::Env)> fn) {
|
|
105
|
+
if (released_ || inactive_.load(std::memory_order_acquire)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
auto* data = new CallData();
|
|
109
|
+
data->type = CallType::kBarrier;
|
|
110
|
+
data->hasMessage = false;
|
|
111
|
+
data->barrier = std::move(fn);
|
|
112
|
+
napi_status status = tsfn_.NonBlockingCall(data);
|
|
113
|
+
if (status != napi_ok) {
|
|
114
|
+
delete data;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// The loop must not exit before the barrier has run even if the user
|
|
118
|
+
// unref()'d the engine; after Release() the TSFN closes as soon as its queue
|
|
119
|
+
// is drained, so this only keeps the loop alive for that drain. Ref'd only
|
|
120
|
+
// once the item is queued: a refused call leaves the TSFN exactly as the
|
|
121
|
+
// caller had it (JS thread, so nothing runs in between).
|
|
122
|
+
tsfn_.Ref(env);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
void ApplicationBridge::Deactivate() {
|
|
127
|
+
inactive_.store(true, std::memory_order_release);
|
|
128
|
+
// Release any network thread already blocked in CallSync so the subsequent
|
|
129
|
+
// engine stop() (which joins those threads) can complete without deadlocking
|
|
130
|
+
// on the frozen main loop.
|
|
131
|
+
WakeAllPending();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bool ApplicationBridge::RegisterChannel(const std::shared_ptr<SyncChannel>& ch,
|
|
135
|
+
std::uint64_t& id) {
|
|
136
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
137
|
+
// Checked under the lock: Deactivate()/Abort()/Release() store inactive_
|
|
138
|
+
// before they take pendingMtx_ for WakeAllPending(), so either this
|
|
139
|
+
// registration lands in pending_ before the wake loop iterates it, or it
|
|
140
|
+
// observes inactive_ here and is refused. Without this re-check a caller
|
|
141
|
+
// could pass CallSync's unlocked fast-path check, register after the wake
|
|
142
|
+
// loop ran, and wait until Abort() while the JS thread is blocked on it.
|
|
143
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
id = nextChannelId_++;
|
|
147
|
+
pending_[id] = ch;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void ApplicationBridge::UnregisterChannel(std::uint64_t id) {
|
|
152
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
153
|
+
pending_.erase(id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
void ApplicationBridge::WakeAllPending() {
|
|
157
|
+
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
158
|
+
for (auto& kv : pending_) {
|
|
159
|
+
auto& ch = kv.second;
|
|
160
|
+
{
|
|
161
|
+
std::lock_guard<std::mutex> chLock(ch->mtx);
|
|
162
|
+
if (!ch->done) {
|
|
163
|
+
ch->done = true; // pass-through (default Result: no mutate, no throw)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
ch->cv.notify_all();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void ApplicationBridge::Ref(Napi::Env env) {
|
|
171
|
+
if (!released_) {
|
|
172
|
+
tsfn_.Ref(env);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
void ApplicationBridge::Unref(Napi::Env env) {
|
|
177
|
+
if (!released_) {
|
|
178
|
+
tsfn_.Unref(env);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// FIX::Application overrides (invoked on QuickFIX threads).
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
void ApplicationBridge::FireAndForget(CallType type, const FIX::SessionID& id) {
|
|
187
|
+
// Bridge is releasing/aborting (possibly during finalize where the TSFN is
|
|
188
|
+
// being destroyed): don't touch the TSFN. Fire-and-forget just drops.
|
|
189
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
auto* data = new CallData();
|
|
193
|
+
data->type = type;
|
|
194
|
+
data->sessionID = id;
|
|
195
|
+
data->hasMessage = false;
|
|
196
|
+
// channel stays null: fire-and-forget.
|
|
197
|
+
napi_status status = tsfn_.NonBlockingCall(data);
|
|
198
|
+
if (status != napi_ok) {
|
|
199
|
+
delete data; // TSFN won't take ownership if the call failed.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ApplicationBridge::Result ApplicationBridge::CallSync(
|
|
204
|
+
CallType type, const FIX::SessionID& id, const FIX::Message& message) {
|
|
205
|
+
// Bridge is releasing/aborting: never call into the TSFN (its internals may be
|
|
206
|
+
// torn down during finalize -> calling would abort the process). Return an
|
|
207
|
+
// empty pass-through result so the QuickFIX thread proceeds without mutation
|
|
208
|
+
// or thrown exception. This is only a cheap fast path; the authoritative
|
|
209
|
+
// check is the one RegisterChannel() makes under pendingMtx_.
|
|
210
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
211
|
+
Result r;
|
|
212
|
+
return r;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Everything that can throw (the copy, its serialization, the dictionary
|
|
216
|
+
// lookup) runs before the channel is registered, and `data` is owned until
|
|
217
|
+
// the TSFN takes it: an exception (std::bad_alloc) leaks nothing and leaves
|
|
218
|
+
// no channel in pending_.
|
|
219
|
+
auto data = std::make_unique<CallData>();
|
|
220
|
+
data->type = type;
|
|
221
|
+
data->sessionID = id;
|
|
222
|
+
// Deep copy, nested entries kept typed: the engine parsed them as
|
|
223
|
+
// FIX::Group with the session dictionary, and a plain copy would slice
|
|
224
|
+
// their delimiter away.
|
|
225
|
+
fieldmap::CloneMessage(data->message, message);
|
|
226
|
+
if (type == CallType::kToAdmin || type == CallType::kToApp) {
|
|
227
|
+
// An outbound message gets BodyLength (9) and CheckSum (10) only when
|
|
228
|
+
// Session::sendRaw serializes it, after toAdmin/toApp: serialize the
|
|
229
|
+
// clone so the handler sees them too. The engine recomputes both anyway.
|
|
230
|
+
data->message.toString();
|
|
231
|
+
}
|
|
232
|
+
data->sessionDictionary = SessionDictionary(id);
|
|
233
|
+
data->hasMessage = true;
|
|
234
|
+
|
|
235
|
+
auto channel = std::make_shared<SyncChannel>();
|
|
236
|
+
std::uint64_t channelId = 0;
|
|
237
|
+
if (!RegisterChannel(channel, channelId)) {
|
|
238
|
+
// Lost the race with Deactivate()/Abort(): the wake loop has already run
|
|
239
|
+
// (or is running) over pending_, so waiting now could block until Abort().
|
|
240
|
+
Result r;
|
|
241
|
+
return r;
|
|
242
|
+
}
|
|
243
|
+
data->channel = channel; // shared: trampoline gets its own owning copy
|
|
244
|
+
|
|
245
|
+
napi_status status = tsfn_.BlockingCall(data.get());
|
|
246
|
+
if (status != napi_ok) {
|
|
247
|
+
// TSFN is closing/aborted: it won't take ownership and won't fulfill the
|
|
248
|
+
// channel. Clean up (`data` frees itself) and return a pass-through
|
|
249
|
+
// result.
|
|
250
|
+
UnregisterChannel(channelId);
|
|
251
|
+
Result r;
|
|
252
|
+
return r;
|
|
253
|
+
}
|
|
254
|
+
data.release(); // the trampoline owns it now
|
|
255
|
+
|
|
256
|
+
// Wait for the trampoline (normal completion) or Abort/Deactivate (teardown)
|
|
257
|
+
// to signal the channel. Never blocks forever: WakeAllPending() sets done.
|
|
258
|
+
{
|
|
259
|
+
std::unique_lock<std::mutex> lock(channel->mtx);
|
|
260
|
+
channel->cv.wait(lock, [&channel] { return channel->done; });
|
|
261
|
+
}
|
|
262
|
+
UnregisterChannel(channelId);
|
|
263
|
+
std::lock_guard<std::mutex> lock(channel->mtx);
|
|
264
|
+
return channel->result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
std::shared_ptr<const FIX::DataDictionary> ApplicationBridge::SessionDictionary(
|
|
268
|
+
const FIX::SessionID& id) {
|
|
269
|
+
std::lock_guard<std::mutex> lock(dictionariesMtx_);
|
|
270
|
+
auto found = dictionaries_.find(id);
|
|
271
|
+
if (found != dictionaries_.end()) {
|
|
272
|
+
return found->second;
|
|
273
|
+
}
|
|
274
|
+
FIX::Session* session = FIX::Session::lookupSession(id);
|
|
275
|
+
if (session == nullptr) {
|
|
276
|
+
return nullptr;
|
|
277
|
+
}
|
|
278
|
+
auto provider = std::make_shared<const FIX::DataDictionaryProvider>(
|
|
279
|
+
session->getDataDictionaryProvider());
|
|
280
|
+
std::shared_ptr<const FIX::DataDictionary> dictionary(
|
|
281
|
+
provider, &provider->getSessionDataDictionary(id.getBeginString()));
|
|
282
|
+
dictionaries_.emplace(id, dictionary);
|
|
283
|
+
return dictionary;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
void ApplicationBridge::onCreate(const FIX::SessionID& id) {
|
|
287
|
+
{
|
|
288
|
+
std::lock_guard<std::mutex> lock(dictionariesMtx_);
|
|
289
|
+
dictionaries_.erase(id);
|
|
290
|
+
}
|
|
291
|
+
FireAndForget(CallType::kOnCreate, id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
void ApplicationBridge::onLogon(const FIX::SessionID& id) {
|
|
295
|
+
FireAndForget(CallType::kOnLogon, id);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
void ApplicationBridge::onLogout(const FIX::SessionID& id) {
|
|
299
|
+
FireAndForget(CallType::kOnLogout, id);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
void ApplicationBridge::toAdmin(FIX::Message& message,
|
|
303
|
+
const FIX::SessionID& id) {
|
|
304
|
+
Result r = CallSync(CallType::kToAdmin, id, message);
|
|
305
|
+
if (r.mutated) {
|
|
306
|
+
message = r.editedMessage;
|
|
307
|
+
}
|
|
308
|
+
// toAdmin doesn't throw in the FIX interface; ignore r.threw here.
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
void ApplicationBridge::toApp(FIX::Message& message, const FIX::SessionID& id)
|
|
312
|
+
EXCEPT(FIX::DoNotSend) {
|
|
313
|
+
Result r = CallSync(CallType::kToApp, id, message);
|
|
314
|
+
if (r.threw) {
|
|
315
|
+
// toApp may throw DoNotSend.
|
|
316
|
+
throw FIX::DoNotSend();
|
|
317
|
+
}
|
|
318
|
+
if (r.mutated) {
|
|
319
|
+
message = r.editedMessage;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
void ApplicationBridge::fromAdmin(const FIX::Message& message,
|
|
324
|
+
const FIX::SessionID& id)
|
|
325
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
326
|
+
FIX::RejectLogon) {
|
|
327
|
+
Result r = CallSync(CallType::kFromAdmin, id, message);
|
|
328
|
+
if (r.threw) {
|
|
329
|
+
if (r.errorName == "RejectLogon") throw FIX::RejectLogon(r.errorMessage);
|
|
330
|
+
if (r.errorName == "IncorrectDataFormat") throw FIX::IncorrectDataFormat(0);
|
|
331
|
+
if (r.errorName == "IncorrectTagValue") throw FIX::IncorrectTagValue(0);
|
|
332
|
+
if (r.errorName == "FieldNotFound") throw FIX::FieldNotFound(0);
|
|
333
|
+
// Default: reject the logon so the peer sees a definitive response.
|
|
334
|
+
throw FIX::RejectLogon(r.errorMessage);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
void ApplicationBridge::fromApp(const FIX::Message& message,
|
|
339
|
+
const FIX::SessionID& id)
|
|
340
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
341
|
+
FIX::UnsupportedMessageType) {
|
|
342
|
+
Result r = CallSync(CallType::kFromApp, id, message);
|
|
343
|
+
if (r.threw) {
|
|
344
|
+
if (r.errorName == "UnsupportedMessageType")
|
|
345
|
+
throw FIX::UnsupportedMessageType();
|
|
346
|
+
if (r.errorName == "IncorrectDataFormat") throw FIX::IncorrectDataFormat(0);
|
|
347
|
+
if (r.errorName == "IncorrectTagValue") throw FIX::IncorrectTagValue(0);
|
|
348
|
+
if (r.errorName == "FieldNotFound") throw FIX::FieldNotFound(0);
|
|
349
|
+
throw FIX::UnsupportedMessageType();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// JS-thread trampoline. Runs on the Node event-loop thread.
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
void ApplicationBridge::CallJs(Napi::Env env, Napi::Function /*jsCallback*/,
|
|
358
|
+
ApplicationBridge* context, CallData* data) {
|
|
359
|
+
// RAII: always delete the CallData and, for sync calls, ALWAYS signal the
|
|
360
|
+
// channel even on early return or a thrown C++ exception in this trampoline.
|
|
361
|
+
// The signal is a no-op if Abort/Deactivate already woke the waiter (done set)
|
|
362
|
+
// — the waiter has already returned pass-through and discarded the channel.
|
|
363
|
+
struct Guard {
|
|
364
|
+
CallData* data;
|
|
365
|
+
Result result;
|
|
366
|
+
~Guard() {
|
|
367
|
+
if (data->channel) {
|
|
368
|
+
auto& ch = *data->channel;
|
|
369
|
+
{
|
|
370
|
+
std::lock_guard<std::mutex> lock(ch.mtx);
|
|
371
|
+
if (!ch.done) {
|
|
372
|
+
ch.result = std::move(result);
|
|
373
|
+
ch.done = true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
ch.cv.notify_all();
|
|
377
|
+
}
|
|
378
|
+
delete data;
|
|
379
|
+
}
|
|
380
|
+
} guard{data, {}};
|
|
381
|
+
|
|
382
|
+
if (data->type == CallType::kBarrier) {
|
|
383
|
+
// Dropped (null env) or reached in FIFO order: either way, hand over.
|
|
384
|
+
if (data->barrier) data->barrier(env);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// env may be null during environment teardown; nothing safe to do.
|
|
389
|
+
if (env == nullptr || context == nullptr) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const bool isSync = (data->channel != nullptr);
|
|
394
|
+
|
|
395
|
+
Napi::HandleScope scope(env);
|
|
396
|
+
|
|
397
|
+
// Resolve the handler function, if the user provided one.
|
|
398
|
+
Napi::Function handler;
|
|
399
|
+
if (!context->handlers_.IsEmpty()) {
|
|
400
|
+
Napi::Object handlers = context->handlers_.Value();
|
|
401
|
+
Napi::Value h = handlers.Get(HandlerName(data->type));
|
|
402
|
+
if (h.IsFunction()) {
|
|
403
|
+
handler = h.As<Napi::Function>();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Build the JS arguments: (message?, sessionID).
|
|
408
|
+
Napi::Object sessionIdObj = SessionIDWrap::NewInstance(env, data->sessionID);
|
|
409
|
+
|
|
410
|
+
Napi::Object msgObj;
|
|
411
|
+
bool haveMsg = false;
|
|
412
|
+
if (data->hasMessage) {
|
|
413
|
+
// Hand the structured copy over to the wrapper (data is deleted by the
|
|
414
|
+
// guard below): no string round-trip and no second copy, so the repeating
|
|
415
|
+
// groups the engine parsed stay intact for the handler.
|
|
416
|
+
msgObj = MessageWrap::NewInstance(env, std::move(data->message),
|
|
417
|
+
std::move(data->sessionDictionary));
|
|
418
|
+
haveMsg = true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (handler.IsEmpty()) {
|
|
422
|
+
// No handler for this callback: pass-through (no mutation, no throw).
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
if (haveMsg) {
|
|
428
|
+
handler.Call({msgObj, sessionIdObj});
|
|
429
|
+
} else {
|
|
430
|
+
handler.Call({sessionIdObj});
|
|
431
|
+
}
|
|
432
|
+
} catch (const Napi::Error& e) {
|
|
433
|
+
if (isSync) {
|
|
434
|
+
guard.result.threw = true;
|
|
435
|
+
// Read a `.fixError` / name off the JS error to pick the FIX exception.
|
|
436
|
+
Napi::Value nameVal;
|
|
437
|
+
try {
|
|
438
|
+
nameVal = e.Value().As<Napi::Object>().Get("fixError");
|
|
439
|
+
} catch (...) {
|
|
440
|
+
}
|
|
441
|
+
if (nameVal.IsString()) {
|
|
442
|
+
guard.result.errorName = nameVal.As<Napi::String>().Utf8Value();
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
guard.result.errorMessage = e.Message();
|
|
446
|
+
} catch (...) {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// For fire-and-forget, the JS error is swallowed here (there's no FIX-thread
|
|
450
|
+
// waiting). Reported best-effort by leaving it; QuickFIX doesn't care.
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// For mutating sync callbacks, read back the (possibly edited) message.
|
|
454
|
+
if (isSync && IsMutating(data->type) && haveMsg && !guard.result.threw) {
|
|
455
|
+
try {
|
|
456
|
+
MessageWrap* w = Napi::ObjectWrap<MessageWrap>::Unwrap(msgObj);
|
|
457
|
+
guard.result.editedMessage = w->Message();
|
|
458
|
+
guard.result.mutated = true;
|
|
459
|
+
} catch (...) {
|
|
460
|
+
guard.result.mutated = false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// guard destructor signals the channel (for sync) and deletes data.
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
} // namespace napi_quickfix
|