@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
|
@@ -0,0 +1,204 @@
|
|
|
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 <functional>
|
|
10
|
+
#include <map>
|
|
11
|
+
#include <memory>
|
|
12
|
+
#include <mutex>
|
|
13
|
+
#include <string>
|
|
14
|
+
#include <unordered_map>
|
|
15
|
+
|
|
16
|
+
#include "quickfix/Application.h"
|
|
17
|
+
#include "quickfix/DataDictionary.h"
|
|
18
|
+
#include "quickfix/Message.h"
|
|
19
|
+
#include "quickfix/SessionID.h"
|
|
20
|
+
|
|
21
|
+
namespace napi_quickfix {
|
|
22
|
+
|
|
23
|
+
// Bridges QuickFIX's FIX::Application callbacks (invoked on QuickFIX's own
|
|
24
|
+
// network threads) to a set of JS handler functions, via a SINGLE
|
|
25
|
+
// TypedThreadSafeFunction dispatcher.
|
|
26
|
+
//
|
|
27
|
+
// Fire-and-forget callbacks (onCreate/onLogon/onLogout) use NonBlockingCall and
|
|
28
|
+
// never wait. Synchronous mutate/throw callbacks (toAdmin/toApp/fromAdmin/
|
|
29
|
+
// fromApp) use BlockingCall + a shared SyncChannel (mutex/condition_variable) so
|
|
30
|
+
// the QuickFIX thread observes any message mutation or thrown FIX exception
|
|
31
|
+
// before returning. Crucially, the wait can be released not only by the JS
|
|
32
|
+
// trampoline but also by Deactivate()/Abort() during teardown, so a network
|
|
33
|
+
// thread never blocks forever when the main event loop is frozen (e.g. inside a
|
|
34
|
+
// finalize/cleanup hook). The engine wraps run start/stop/sendToTarget on libuv
|
|
35
|
+
// worker threads (AsyncWorker) so the main loop stays free to service these
|
|
36
|
+
// BlockingCall trampolines — this is what breaks the original deadlock.
|
|
37
|
+
class ApplicationBridge : public FIX::Application {
|
|
38
|
+
public:
|
|
39
|
+
enum class CallType {
|
|
40
|
+
kOnCreate,
|
|
41
|
+
kOnLogon,
|
|
42
|
+
kOnLogout,
|
|
43
|
+
kToAdmin,
|
|
44
|
+
kToApp,
|
|
45
|
+
kFromAdmin,
|
|
46
|
+
kFromApp,
|
|
47
|
+
// Not an Application callback: a closure queued by the JS thread through
|
|
48
|
+
// the same FIFO, so it runs after every event queued before it (see
|
|
49
|
+
// RunAfterQueued).
|
|
50
|
+
kBarrier,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Result returned from the JS trampoline for synchronous calls.
|
|
54
|
+
struct Result {
|
|
55
|
+
// For mutating callbacks (toAdmin/toApp), the (possibly edited) message.
|
|
56
|
+
// Carried as a FIX::Message (not its wire string) so repeating groups the
|
|
57
|
+
// handler added survive: re-parsing a string without a dictionary would
|
|
58
|
+
// flatten them.
|
|
59
|
+
FIX::Message editedMessage;
|
|
60
|
+
bool mutated = false;
|
|
61
|
+
// If the JS handler threw, the FIX exception class name to re-throw
|
|
62
|
+
// (e.g. "DoNotSend", "RejectLogon", "UnsupportedMessageType") and message.
|
|
63
|
+
bool threw = false;
|
|
64
|
+
std::string errorName;
|
|
65
|
+
std::string errorMessage;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Reply channel for a synchronous call. Heap-allocated and shared (shared_ptr)
|
|
69
|
+
// between the waiting QuickFIX thread and the JS trampoline so neither can
|
|
70
|
+
// dangle. Guarded by a mutex + condition_variable so the waiter can be woken
|
|
71
|
+
// either by the trampoline (normal completion) or by Abort()/Deactivate()
|
|
72
|
+
// (teardown), instead of blocking forever on a future when the main loop is
|
|
73
|
+
// frozen inside a cleanup hook.
|
|
74
|
+
struct SyncChannel {
|
|
75
|
+
std::mutex mtx;
|
|
76
|
+
std::condition_variable cv;
|
|
77
|
+
bool done = false;
|
|
78
|
+
Result result;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
struct CallData {
|
|
82
|
+
CallType type;
|
|
83
|
+
FIX::SessionID sessionID; // copied by value — safe across threads
|
|
84
|
+
// A deep copy of the message for message-bearing calls (a
|
|
85
|
+
// fieldmap::CloneMessage, which keeps every nested group entry typed).
|
|
86
|
+
// Copied, not serialized: the engine parsed it with the session's
|
|
87
|
+
// dictionary, and only the structured copy keeps its repeating groups for
|
|
88
|
+
// the JS handler, which takes the copy over.
|
|
89
|
+
FIX::Message message;
|
|
90
|
+
// The session dictionary the JS wrapper routes the message's fields by
|
|
91
|
+
// (see SessionDictionary); null when the session is gone.
|
|
92
|
+
std::shared_ptr<const FIX::DataDictionary> sessionDictionary;
|
|
93
|
+
bool hasMessage = false;
|
|
94
|
+
// For synchronous calls only: shared reply channel (null for fire-and-forget).
|
|
95
|
+
std::shared_ptr<SyncChannel> channel;
|
|
96
|
+
// kBarrier only. Invoked with a null Env if the item is dropped (TSFN
|
|
97
|
+
// aborted / environment teardown): then it must not touch JS.
|
|
98
|
+
std::function<void(Napi::Env)> barrier;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// The JS-thread trampoline. Runs on the Node event-loop thread. Declared
|
|
102
|
+
// before the TSFN alias because it is used as a template argument.
|
|
103
|
+
static void CallJs(Napi::Env env, Napi::Function jsCallback,
|
|
104
|
+
ApplicationBridge* context, CallData* data);
|
|
105
|
+
|
|
106
|
+
using TSFN = Napi::TypedThreadSafeFunction<ApplicationBridge, CallData,
|
|
107
|
+
ApplicationBridge::CallJs>;
|
|
108
|
+
|
|
109
|
+
// Construct on the JS thread. `handlers` is the user's handlers object (may be
|
|
110
|
+
// empty/undefined). `env` is captured for control (Ref/Unref).
|
|
111
|
+
ApplicationBridge(Napi::Env env, Napi::Object handlers);
|
|
112
|
+
~ApplicationBridge() override;
|
|
113
|
+
|
|
114
|
+
// TSFN lifecycle. Acquire is a no-op placeholder (the initial thread count is
|
|
115
|
+
// 1 from New); Release drains, Abort force-stops. Ref/Unref control whether
|
|
116
|
+
// the TSFN keeps the event loop alive.
|
|
117
|
+
void Release();
|
|
118
|
+
void Abort();
|
|
119
|
+
// Mark the bridge inactive WITHOUT touching the TSFN. Callbacks arriving on
|
|
120
|
+
// QuickFIX threads will pass through immediately. Call this on the MAIN thread
|
|
121
|
+
// BEFORE stopping the engine during finalize, so the subsequent engine stop()
|
|
122
|
+
// (which joins the network threads) can't have a callback race into a TSFN
|
|
123
|
+
// that is about to be aborted.
|
|
124
|
+
void Deactivate();
|
|
125
|
+
void Ref(Napi::Env env);
|
|
126
|
+
void Unref(Napi::Env env);
|
|
127
|
+
|
|
128
|
+
// Queue `fn` to run on the JS thread AFTER every callback already queued
|
|
129
|
+
// (the TSFN queue is FIFO), and keep the event loop alive until it has run.
|
|
130
|
+
// JS thread only; call it before Release()/Abort(). Returns false, without
|
|
131
|
+
// queuing anything, if the bridge is no longer accepting work -- the caller
|
|
132
|
+
// then runs `fn` itself. `fn` receives a null Env if the queue is discarded
|
|
133
|
+
// before it is reached (Abort() / environment teardown) and must then do
|
|
134
|
+
// nothing that touches JS.
|
|
135
|
+
bool RunAfterQueued(Napi::Env env, std::function<void(Napi::Env)> fn);
|
|
136
|
+
|
|
137
|
+
// FIX::Application overrides.
|
|
138
|
+
void onCreate(const FIX::SessionID&) override;
|
|
139
|
+
void onLogon(const FIX::SessionID&) override;
|
|
140
|
+
void onLogout(const FIX::SessionID&) override;
|
|
141
|
+
void toAdmin(FIX::Message&, const FIX::SessionID&) override;
|
|
142
|
+
void toApp(FIX::Message&, const FIX::SessionID&) EXCEPT(FIX::DoNotSend) override;
|
|
143
|
+
void fromAdmin(const FIX::Message&, const FIX::SessionID&)
|
|
144
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat,
|
|
145
|
+
FIX::IncorrectTagValue, FIX::RejectLogon) override;
|
|
146
|
+
void fromApp(const FIX::Message&, const FIX::SessionID&)
|
|
147
|
+
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat,
|
|
148
|
+
FIX::IncorrectTagValue, FIX::UnsupportedMessageType) override;
|
|
149
|
+
|
|
150
|
+
private:
|
|
151
|
+
void FireAndForget(CallType type, const FIX::SessionID& id);
|
|
152
|
+
// Runs a synchronous call and returns the Result (throwing FIX exception is
|
|
153
|
+
// handled by the caller-side override, which inspects Result).
|
|
154
|
+
Result CallSync(CallType type, const FIX::SessionID& id,
|
|
155
|
+
const FIX::Message& message);
|
|
156
|
+
|
|
157
|
+
// The session dictionary of `id`, shared so a JS Message can keep it past
|
|
158
|
+
// the session's lifetime: a copy of the session's DataDictionaryProvider
|
|
159
|
+
// (which shares its dictionaries' ownership) aliased to the dictionary for
|
|
160
|
+
// the session's BeginString. Built once per session, on the QuickFIX thread
|
|
161
|
+
// running the callback (the session is alive there); onCreate drops the
|
|
162
|
+
// entry so a recreated session gets its own. Null if the session is not
|
|
163
|
+
// registered.
|
|
164
|
+
std::shared_ptr<const FIX::DataDictionary> SessionDictionary(
|
|
165
|
+
const FIX::SessionID& id);
|
|
166
|
+
std::mutex dictionariesMtx_;
|
|
167
|
+
std::map<FIX::SessionID, std::shared_ptr<const FIX::DataDictionary>>
|
|
168
|
+
dictionaries_;
|
|
169
|
+
|
|
170
|
+
Napi::ObjectReference handlers_;
|
|
171
|
+
TSFN tsfn_;
|
|
172
|
+
bool released_ = false;
|
|
173
|
+
// Set true by Abort()/Release() (which may run on the MAIN thread during
|
|
174
|
+
// finalize) and read by the FIX::Application overrides (which run on QuickFIX
|
|
175
|
+
// network threads). Once true, the overrides must NOT touch the TSFN — during
|
|
176
|
+
// environment teardown the TSFN's internals may already be destroyed, and
|
|
177
|
+
// calling into it aborts the process. Atomic so the cross-thread read is safe.
|
|
178
|
+
std::atomic<bool> inactive_{false};
|
|
179
|
+
|
|
180
|
+
// Wake all in-flight synchronous callers (used by Deactivate/Abort so a
|
|
181
|
+
// network thread blocked in CallSync doesn't wait forever when the main loop
|
|
182
|
+
// is frozen inside a cleanup hook). Registers each SyncChannel while pending.
|
|
183
|
+
//
|
|
184
|
+
// Invariant: every path that sets inactive_ stores it BEFORE taking
|
|
185
|
+
// pendingMtx_ to run WakeAllPending(), and RegisterChannel() re-checks
|
|
186
|
+
// inactive_ UNDER pendingMtx_. So a registration that saw inactive_ unset is
|
|
187
|
+
// already in pending_ when the wake loop runs, and one that lost the race is
|
|
188
|
+
// refused; either way no caller can start waiting after the wake. The JS
|
|
189
|
+
// thread relies on this when it blocks on a network thread after
|
|
190
|
+
// Deactivate() (Teardown's stop() join, DestroyEngine's gate freeze).
|
|
191
|
+
std::mutex pendingMtx_;
|
|
192
|
+
std::unordered_map<std::uint64_t, std::shared_ptr<SyncChannel>> pending_;
|
|
193
|
+
std::uint64_t nextChannelId_ = 0;
|
|
194
|
+
|
|
195
|
+
// Returns false (nothing registered) if the bridge is already inactive.
|
|
196
|
+
bool RegisterChannel(const std::shared_ptr<SyncChannel>& ch,
|
|
197
|
+
std::uint64_t& id);
|
|
198
|
+
void UnregisterChannel(std::uint64_t id);
|
|
199
|
+
void WakeAllPending();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
} // namespace napi_quickfix
|
|
203
|
+
|
|
204
|
+
#endif // NAPI_QUICKFIX_APPLICATION_BRIDGE_H
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#include "data_dictionary_wrap.h"
|
|
2
|
+
|
|
3
|
+
#include <sstream>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "errors.h"
|
|
7
|
+
#include "field_map_ops.h"
|
|
8
|
+
#include "message_wrap.h"
|
|
9
|
+
|
|
10
|
+
namespace napi_quickfix {
|
|
11
|
+
|
|
12
|
+
Napi::FunctionReference DataDictionaryWrap::constructor_;
|
|
13
|
+
|
|
14
|
+
Napi::Object DataDictionaryWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
15
|
+
Napi::Function func = DefineClass(
|
|
16
|
+
env, "DataDictionary",
|
|
17
|
+
{
|
|
18
|
+
StaticMethod("fromFile", &DataDictionaryWrap::FromFile),
|
|
19
|
+
StaticMethod("fromString", &DataDictionaryWrap::FromString),
|
|
20
|
+
InstanceMethod("validate", &DataDictionaryWrap::Validate),
|
|
21
|
+
InstanceMethod("getVersion", &DataDictionaryWrap::GetVersion),
|
|
22
|
+
InstanceMethod("getFieldName", &DataDictionaryWrap::GetFieldName),
|
|
23
|
+
InstanceMethod("getFieldTag", &DataDictionaryWrap::GetFieldTag),
|
|
24
|
+
InstanceMethod("isField", &DataDictionaryWrap::IsField),
|
|
25
|
+
InstanceMethod("isMsgType", &DataDictionaryWrap::IsMsgType),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
constructor_ = Napi::Persistent(func);
|
|
29
|
+
constructor_.SuppressDestruct();
|
|
30
|
+
|
|
31
|
+
exports.Set("DataDictionaryWrap", func);
|
|
32
|
+
return exports;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DataDictionaryWrap* DataDictionaryWrap::UnwrapArg(Napi::Env env,
|
|
36
|
+
Napi::Value value,
|
|
37
|
+
const char* argName) {
|
|
38
|
+
if (!value.IsObject() ||
|
|
39
|
+
!value.As<Napi::Object>().InstanceOf(constructor_.Value())) {
|
|
40
|
+
throw Napi::TypeError::New(
|
|
41
|
+
env, std::string(argName) + " must be a DataDictionary instance");
|
|
42
|
+
}
|
|
43
|
+
return Napi::ObjectWrap<DataDictionaryWrap>::Unwrap(value.As<Napi::Object>());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Constructor is internal: (kind: "string"|"file", payload: string).
|
|
47
|
+
DataDictionaryWrap::DataDictionaryWrap(const Napi::CallbackInfo& info)
|
|
48
|
+
: Napi::ObjectWrap<DataDictionaryWrap>(info) {
|
|
49
|
+
Napi::Env env = info.Env();
|
|
50
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
51
|
+
throw Napi::TypeError::New(
|
|
52
|
+
env,
|
|
53
|
+
"DataDictionary cannot be constructed directly; use "
|
|
54
|
+
"DataDictionary.fromFile() or DataDictionary.fromString()");
|
|
55
|
+
}
|
|
56
|
+
std::string kind = info[0].As<Napi::String>().Utf8Value();
|
|
57
|
+
std::string payload = info[1].As<Napi::String>().Utf8Value();
|
|
58
|
+
|
|
59
|
+
NQ_TRY(env) {
|
|
60
|
+
if (kind == "string") {
|
|
61
|
+
std::istringstream stream(payload);
|
|
62
|
+
dict_->readFromStream(stream);
|
|
63
|
+
} else { // "file"
|
|
64
|
+
dict_->readFromURL(payload);
|
|
65
|
+
}
|
|
66
|
+
} NQ_CATCH(env)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Napi::Value DataDictionaryWrap::FromFile(const Napi::CallbackInfo& info) {
|
|
70
|
+
Napi::Env env = info.Env();
|
|
71
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
72
|
+
throw Napi::TypeError::New(env, "fromFile(path: string)");
|
|
73
|
+
}
|
|
74
|
+
return constructor_.New(
|
|
75
|
+
{Napi::String::New(env, "file"), info[0].As<Napi::String>()});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Napi::Value DataDictionaryWrap::FromString(const Napi::CallbackInfo& info) {
|
|
79
|
+
Napi::Env env = info.Env();
|
|
80
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
81
|
+
throw Napi::TypeError::New(env, "fromString(xml: string)");
|
|
82
|
+
}
|
|
83
|
+
return constructor_.New(
|
|
84
|
+
{Napi::String::New(env, "string"), info[0].As<Napi::String>()});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// validate(message, bodyOnly?: boolean). With bodyOnly the dictionary is used
|
|
88
|
+
// as the application dictionary only (QuickFIX's `validate(msg, true)`): no
|
|
89
|
+
// BeginString version check and no tag/value validation of the header and
|
|
90
|
+
// trailer fields. The header still needs BeginString and MsgType, and every
|
|
91
|
+
// field the dictionary requires, header and trailer included, must be present.
|
|
92
|
+
Napi::Value DataDictionaryWrap::Validate(const Napi::CallbackInfo& info) {
|
|
93
|
+
Napi::Env env = info.Env();
|
|
94
|
+
MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
|
|
95
|
+
bool bodyOnly = false;
|
|
96
|
+
if (info.Length() >= 2 && !fieldmap::IsNullish(info[1])) {
|
|
97
|
+
bodyOnly = info[1].ToBoolean().Value();
|
|
98
|
+
}
|
|
99
|
+
NQ_TRY(env) {
|
|
100
|
+
dict_->validate(msg->Message(), bodyOnly);
|
|
101
|
+
} NQ_CATCH(env)
|
|
102
|
+
return env.Undefined();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The BeginString the spec declares (e.g. "FIX.4.4"), "" if it has none.
|
|
106
|
+
Napi::Value DataDictionaryWrap::GetVersion(const Napi::CallbackInfo& info) {
|
|
107
|
+
Napi::Env env = info.Env();
|
|
108
|
+
NQ_TRY(env) {
|
|
109
|
+
return Napi::String::New(env, dict_->getVersion());
|
|
110
|
+
} NQ_CATCH(env)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Napi::Value DataDictionaryWrap::GetFieldName(const Napi::CallbackInfo& info) {
|
|
114
|
+
Napi::Env env = info.Env();
|
|
115
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
116
|
+
throw Napi::TypeError::New(env, "getFieldName(tag: number)");
|
|
117
|
+
}
|
|
118
|
+
int tag = fieldmap::CoerceTag(env, info[0]);
|
|
119
|
+
NQ_TRY(env) {
|
|
120
|
+
std::string name;
|
|
121
|
+
if (!dict_->getFieldName(tag, name)) {
|
|
122
|
+
return env.Undefined();
|
|
123
|
+
}
|
|
124
|
+
return Napi::String::New(env, name);
|
|
125
|
+
} NQ_CATCH(env)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Napi::Value DataDictionaryWrap::GetFieldTag(const Napi::CallbackInfo& info) {
|
|
129
|
+
Napi::Env env = info.Env();
|
|
130
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
131
|
+
throw Napi::TypeError::New(env, "getFieldTag(name: string)");
|
|
132
|
+
}
|
|
133
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
134
|
+
NQ_TRY(env) {
|
|
135
|
+
int tag = 0;
|
|
136
|
+
if (!dict_->getFieldTag(name, tag)) {
|
|
137
|
+
return env.Undefined();
|
|
138
|
+
}
|
|
139
|
+
return Napi::Number::New(env, tag);
|
|
140
|
+
} NQ_CATCH(env)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Napi::Value DataDictionaryWrap::IsField(const Napi::CallbackInfo& info) {
|
|
144
|
+
Napi::Env env = info.Env();
|
|
145
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
146
|
+
throw Napi::TypeError::New(env, "isField(tag: number)");
|
|
147
|
+
}
|
|
148
|
+
int tag = fieldmap::CoerceTag(env, info[0]);
|
|
149
|
+
NQ_TRY(env) {
|
|
150
|
+
return Napi::Boolean::New(env, dict_->isField(tag));
|
|
151
|
+
} NQ_CATCH(env)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Napi::Value DataDictionaryWrap::IsMsgType(const Napi::CallbackInfo& info) {
|
|
155
|
+
Napi::Env env = info.Env();
|
|
156
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
157
|
+
throw Napi::TypeError::New(env, "isMsgType(msgType: string)");
|
|
158
|
+
}
|
|
159
|
+
std::string msgType = info[0].As<Napi::String>().Utf8Value();
|
|
160
|
+
NQ_TRY(env) {
|
|
161
|
+
return Napi::Boolean::New(env, dict_->isMsgType(msgType));
|
|
162
|
+
} NQ_CATCH(env)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
} // namespace napi_quickfix
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
2
|
+
#define NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include <memory>
|
|
7
|
+
|
|
8
|
+
#include "quickfix/DataDictionary.h"
|
|
9
|
+
|
|
10
|
+
namespace napi_quickfix {
|
|
11
|
+
|
|
12
|
+
// Napi::ObjectWrap over FIX::DataDictionary.
|
|
13
|
+
//
|
|
14
|
+
// DataDictionary.fromFile(path): DataDictionary
|
|
15
|
+
// DataDictionary.fromString(xml): DataDictionary
|
|
16
|
+
// dd.validate(message: Message, bodyOnly?: boolean): void
|
|
17
|
+
// (throws QuickFixError if invalid)
|
|
18
|
+
// dd.getVersion(): string
|
|
19
|
+
// dd.getFieldName(tag): string | undefined
|
|
20
|
+
// dd.getFieldTag(name): number | undefined
|
|
21
|
+
// dd.isField(tag): boolean
|
|
22
|
+
// dd.isMsgType(msgType): boolean
|
|
23
|
+
class DataDictionaryWrap : public Napi::ObjectWrap<DataDictionaryWrap> {
|
|
24
|
+
public:
|
|
25
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
26
|
+
|
|
27
|
+
explicit DataDictionaryWrap(const Napi::CallbackInfo& info);
|
|
28
|
+
|
|
29
|
+
FIX::DataDictionary& Dictionary() { return *dict_; }
|
|
30
|
+
const FIX::DataDictionary& Dictionary() const { return *dict_; }
|
|
31
|
+
// Shared ownership, for a Message that routes its fields by this dictionary
|
|
32
|
+
// and may outlive the JS DataDictionary.
|
|
33
|
+
std::shared_ptr<const FIX::DataDictionary> Shared() const { return dict_; }
|
|
34
|
+
|
|
35
|
+
// Unwrap a JS value that must be a DataDictionaryWrap; throws
|
|
36
|
+
// Napi::TypeError if not.
|
|
37
|
+
static DataDictionaryWrap* UnwrapArg(Napi::Env env, Napi::Value value,
|
|
38
|
+
const char* argName);
|
|
39
|
+
|
|
40
|
+
private:
|
|
41
|
+
static Napi::FunctionReference constructor_;
|
|
42
|
+
|
|
43
|
+
static Napi::Value FromFile(const Napi::CallbackInfo& info);
|
|
44
|
+
static Napi::Value FromString(const Napi::CallbackInfo& info);
|
|
45
|
+
|
|
46
|
+
Napi::Value Validate(const Napi::CallbackInfo& info);
|
|
47
|
+
Napi::Value GetVersion(const Napi::CallbackInfo& info);
|
|
48
|
+
Napi::Value GetFieldName(const Napi::CallbackInfo& info);
|
|
49
|
+
Napi::Value GetFieldTag(const Napi::CallbackInfo& info);
|
|
50
|
+
Napi::Value IsField(const Napi::CallbackInfo& info);
|
|
51
|
+
Napi::Value IsMsgType(const Napi::CallbackInfo& info);
|
|
52
|
+
|
|
53
|
+
std::shared_ptr<FIX::DataDictionary> dict_ =
|
|
54
|
+
std::make_shared<FIX::DataDictionary>();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
} // namespace napi_quickfix
|
|
58
|
+
|
|
59
|
+
#endif // NAPI_QUICKFIX_DATA_DICTIONARY_WRAP_H
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_ENGINE_COMMON_H
|
|
2
|
+
#define NAPI_QUICKFIX_ENGINE_COMMON_H
|
|
3
|
+
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <set>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <utility>
|
|
8
|
+
|
|
9
|
+
#include "quickfix/Exceptions.h"
|
|
10
|
+
#include "quickfix/Log.h"
|
|
11
|
+
#include "quickfix/Session.h"
|
|
12
|
+
#include "quickfix/SessionID.h"
|
|
13
|
+
#include "quickfix/SessionSettings.h"
|
|
14
|
+
|
|
15
|
+
namespace napi_quickfix {
|
|
16
|
+
|
|
17
|
+
// Throw ConfigError("Duplicate Session ...") if any [SESSION] of `settings`
|
|
18
|
+
// that an engine of `connectionType` ("initiator" / "acceptor") would create
|
|
19
|
+
// already has a live FIX::Session in this process. QuickFIX only rejects
|
|
20
|
+
// duplicates within ONE settings object (SessionSettings::set); across engines
|
|
21
|
+
// its Session constructor ignores addSession()'s result, so a second engine
|
|
22
|
+
// would silently shadow the first in the process-wide registry that
|
|
23
|
+
// lookupSession()/sendToTarget() read, and the per-engine SessionOpGate
|
|
24
|
+
// registry would couple operations with the wrong engine. Call it on the JS
|
|
25
|
+
// thread right before constructing the engine: engines are only created and
|
|
26
|
+
// destroyed on that thread, so the check cannot race another engine.
|
|
27
|
+
inline void RejectDuplicateSessions(const FIX::SessionSettings& settings,
|
|
28
|
+
const std::string& connectionType) {
|
|
29
|
+
for (const FIX::SessionID& id : settings.getSessions()) {
|
|
30
|
+
const FIX::Dictionary& dict = settings.get(id);
|
|
31
|
+
// Sessions of the other type are skipped by the engine (and a missing
|
|
32
|
+
// ConnectionType is its own ConfigError, raised by the engine itself).
|
|
33
|
+
if (!dict.has("ConnectionType") ||
|
|
34
|
+
dict.getString("ConnectionType") != connectionType) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (FIX::Session::doesSessionExist(id)) {
|
|
38
|
+
throw FIX::ConfigError("Duplicate Session " + id.toString());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Delete the FIX::Session objects a FAILED engine constructor left behind.
|
|
44
|
+
// FIX::Initiator/Acceptor::initialize() creates one FIX::Session per [SESSION]
|
|
45
|
+
// of its connection type, registering each in the process-wide registry, and
|
|
46
|
+
// if a later [SESSION] throws ConfigError the constructor unwinds WITHOUT the
|
|
47
|
+
// engine destructor that would delete them: they stay registered with no
|
|
48
|
+
// owner, so lookupSession() keeps resolving them and every later engine with
|
|
49
|
+
// one of those ids is rejected as a duplicate for the rest of the process.
|
|
50
|
+
// Call it right after the engine constructor threw, on the JS thread, with
|
|
51
|
+
// the same `settings`/`connectionType` RejectDuplicateSessions() was called
|
|
52
|
+
// with just before: that check guarantees every session of ours that exists
|
|
53
|
+
// now was created by the failed constructor, and each one is fully
|
|
54
|
+
// constructed: the only callback FIX::Session's constructor makes after
|
|
55
|
+
// registering itself is onCreate, which the bridge dispatches fire-and-forget
|
|
56
|
+
// (a throwing JS handler never unwinds the constructor). ~Session unregisters
|
|
57
|
+
// it and destroys its store/log through the factories the wrap still owns
|
|
58
|
+
// (public virtual destructor; the engine destructor does exactly this
|
|
59
|
+
// `delete`).
|
|
60
|
+
inline void DestroyOrphanedSessions(const FIX::SessionSettings& settings,
|
|
61
|
+
const std::string& connectionType) {
|
|
62
|
+
for (const FIX::SessionID& id : settings.getSessions()) {
|
|
63
|
+
const FIX::Dictionary& dict = settings.get(id);
|
|
64
|
+
if (!dict.has("ConnectionType") ||
|
|
65
|
+
dict.getString("ConnectionType") != connectionType) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
delete FIX::Session::lookupSession(id); // nullptr-safe
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A LogFactory that produces no-op NullLog instances, for log:'none'. QuickFIX
|
|
73
|
+
// ships NullLog but no public NullLogFactory, so we provide one.
|
|
74
|
+
class NullLogFactory : public FIX::LogFactory {
|
|
75
|
+
public:
|
|
76
|
+
FIX::Log* create() override { return new FIX::NullLog(); }
|
|
77
|
+
FIX::Log* create(const FIX::SessionID&) override { return new FIX::NullLog(); }
|
|
78
|
+
void destroy(FIX::Log* log) override { delete log; }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// The wrap's LogFactory: forwards to the real one (File/Screen/Null) and
|
|
82
|
+
// remembers the ENGINE-level Log that FIX::Initiator/Acceptor's constructor
|
|
83
|
+
// requests with create() -- no SessionID -- before it runs initialize(). When
|
|
84
|
+
// initialize() throws ConfigError the constructor unwinds without the engine
|
|
85
|
+
// destructor that would hand that Log back (`m_pLogFactory->destroy(m_pLog)`),
|
|
86
|
+
// and QuickFIX's own factories do not reclaim it either (~FileLogFactory does
|
|
87
|
+
// not delete its m_globalLog): with log:'file' every failed construction left
|
|
88
|
+
// two open streams behind. The wrap calls DestroyOrphanedEngineLog() next to
|
|
89
|
+
// DestroyOrphanedSessions() when the engine constructor threw. The engine
|
|
90
|
+
// keeps a raw pointer to its LogFactory for its whole life (per-session logs,
|
|
91
|
+
// and the destroy() in its destructor), so this object must outlive it: it is
|
|
92
|
+
// the wrap's logFactory_ member, declared before the engine.
|
|
93
|
+
class TrackingLogFactory : public FIX::LogFactory {
|
|
94
|
+
public:
|
|
95
|
+
explicit TrackingLogFactory(std::unique_ptr<FIX::LogFactory> inner)
|
|
96
|
+
: inner_(std::move(inner)) {}
|
|
97
|
+
|
|
98
|
+
FIX::Log* create() override {
|
|
99
|
+
FIX::Log* log = inner_->create(); // may throw: nothing recorded then
|
|
100
|
+
engineLog_ = log;
|
|
101
|
+
return log;
|
|
102
|
+
}
|
|
103
|
+
FIX::Log* create(const FIX::SessionID& id) override {
|
|
104
|
+
return inner_->create(id);
|
|
105
|
+
}
|
|
106
|
+
void destroy(FIX::Log* log) override {
|
|
107
|
+
if (log == engineLog_) engineLog_ = nullptr;
|
|
108
|
+
inner_->destroy(log);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Destroy the engine-level Log if the engine constructor requested one and
|
|
112
|
+
// then threw, so no engine will ever destroy() it. No-op otherwise.
|
|
113
|
+
void DestroyOrphanedEngineLog() {
|
|
114
|
+
if (engineLog_ == nullptr) return;
|
|
115
|
+
FIX::Log* log = engineLog_;
|
|
116
|
+
engineLog_ = nullptr;
|
|
117
|
+
inner_->destroy(log);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private:
|
|
121
|
+
std::unique_ptr<FIX::LogFactory> inner_;
|
|
122
|
+
FIX::Log* engineLog_ = nullptr;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
} // namespace napi_quickfix
|
|
126
|
+
|
|
127
|
+
#endif // NAPI_QUICKFIX_ENGINE_COMMON_H
|