@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,201 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_SESSION_OP_GATE_H
|
|
2
|
+
#define NAPI_QUICKFIX_SESSION_OP_GATE_H
|
|
3
|
+
|
|
4
|
+
#include <condition_variable>
|
|
5
|
+
#include <map>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <set>
|
|
9
|
+
#include <utility>
|
|
10
|
+
|
|
11
|
+
#include "quickfix/SessionID.h"
|
|
12
|
+
|
|
13
|
+
namespace napi_quickfix {
|
|
14
|
+
|
|
15
|
+
// Per-engine gate between the libuv-thread session operations
|
|
16
|
+
// (SendToTargetWorker, session_static.cpp) and the destruction of the engine
|
|
17
|
+
// that owns the sessions they operate on.
|
|
18
|
+
//
|
|
19
|
+
// Why: FIX::Session::lookupSession() hands out a raw, non-owning pointer into
|
|
20
|
+
// an engine-owned object, and nothing in QuickFIX keeps that session alive
|
|
21
|
+
// while the caller uses it. The engine wraps destroy their FIX engine (which
|
|
22
|
+
// deletes every FIX::Session) as soon as stop() settles, so without this gate
|
|
23
|
+
// a `sendToTarget()` still running on one libuv thread while `engine.stop()`
|
|
24
|
+
// completes would be a use-after-free.
|
|
25
|
+
//
|
|
26
|
+
// Why per engine (not process-wide): the gate must only ever couple an
|
|
27
|
+
// operation with the engine that owns its session. A process-wide gate made
|
|
28
|
+
// `A.stop()` wait for B's in-flight operations, and -- worse -- made the wrap
|
|
29
|
+
// destructor, which runs on the JS thread at GC, wait for an operation of
|
|
30
|
+
// ANOTHER engine that may itself be blocked in a BlockingCall that only the JS
|
|
31
|
+
// thread can service: a hard deadlock. With one gate per engine, the
|
|
32
|
+
// destructor only waits for operations on its own sessions, and Teardown()
|
|
33
|
+
// has already deactivated this engine's bridge and joined its network threads
|
|
34
|
+
// by then, so none of those can be waiting on the JS thread.
|
|
35
|
+
//
|
|
36
|
+
// Protocol:
|
|
37
|
+
// * Each engine wrap owns a `std::shared_ptr<SessionOpGate>` and registers
|
|
38
|
+
// it for its SessionIDs (Register) right after constructing the FIX engine,
|
|
39
|
+
// on the JS thread; it unregisters (Unregister) right before destroying it.
|
|
40
|
+
// * The JS-thread entry point of a session operation (SendToTarget) looks
|
|
41
|
+
// the gate of the engine owning `id` up (Lookup) BEFORE queuing its
|
|
42
|
+
// worker, so the operation is bound to the engine that owned the session
|
|
43
|
+
// when it was called: engines register and unregister on the JS thread
|
|
44
|
+
// only, so that lookup is exact, and an engine registered later for the
|
|
45
|
+
// same id can never be the one the operation runs on. No registered gate
|
|
46
|
+
// means no engine owns the session: SessionNotFound, without touching the
|
|
47
|
+
// registry QuickFIX keeps. The worker then calls Acquire(gate, id) on its
|
|
48
|
+
// libuv thread to hold an OpScope on that gate for its whole Execute()
|
|
49
|
+
// -- lookupSession AND the operation. Constructing the OpScope blocks
|
|
50
|
+
// while that engine is between Freeze() and Unfreeze(); a gate that is no
|
|
51
|
+
// longer the registered owner once the scope is held is SessionNotFound
|
|
52
|
+
// too (see below).
|
|
53
|
+
// * The engine's stop worker calls Freeze() on its libuv thread AFTER the FIX
|
|
54
|
+
// engine's own stop() has returned: it blocks new operations on this
|
|
55
|
+
// engine's sessions and waits for the in-flight ones to drain. The wrap
|
|
56
|
+
// then unregisters and destroys the engine on the JS thread (OnOK) and
|
|
57
|
+
// calls Unfreeze() -- or, if the engine survives (the stop threw, or the
|
|
58
|
+
// environment is torn down before a graceful stop's queued destruction
|
|
59
|
+
// runs), calls Unfreeze() alone so that operations parked meanwhile are
|
|
60
|
+
// woken rather than left pinning a libuv thread. Because Freeze() waits
|
|
61
|
+
// on a libuv thread while the JS loop is free, an in-flight operation
|
|
62
|
+
// blocked in a BlockingCall to JS (sendToTarget() -> toApp) can still
|
|
63
|
+
// complete.
|
|
64
|
+
// * The wrap destructor calls Freeze() on the JS thread, but only after
|
|
65
|
+
// Teardown() has deactivated the bridge (waking every pending BlockingCall
|
|
66
|
+
// and making later callbacks pass through) and joined the network threads,
|
|
67
|
+
// so no in-flight operation on THIS engine's sessions can be waiting on the
|
|
68
|
+
// (now blocked) JS thread. Operations on other engines are not involved.
|
|
69
|
+
//
|
|
70
|
+
// The gate outlives its engine: a worker whose gate was resolved just before
|
|
71
|
+
// the engine was unregistered holds it by shared_ptr and passes the (now
|
|
72
|
+
// lifted) freeze. By then a NEW engine may own the same id -- its sessions are what
|
|
73
|
+
// lookupSession() would now return -- while the worker holds the OLD gate, on
|
|
74
|
+
// which the new engine's stop() never waits. Acquire() closes that window by
|
|
75
|
+
// re-reading the registry after the OpScope is held: the scope is kept only if
|
|
76
|
+
// the gate is still the registered owner, and that owner cannot be destroyed
|
|
77
|
+
// while the scope is in flight (Freeze() precedes Unregister() everywhere).
|
|
78
|
+
// Otherwise the operation is reported as SessionNotFound, never retried
|
|
79
|
+
// against the new owner: it was queued for the engine that is now gone, and a
|
|
80
|
+
// message delivered through another engine's session -- not logged on, so
|
|
81
|
+
// persisted in its store and sent on its first logon -- is exactly the
|
|
82
|
+
// cross-over SessionWrap::Resolve() refuses for Session handles. Together
|
|
83
|
+
// with the wraps rejecting a SessionID that is already live in the process,
|
|
84
|
+
// this makes gate == registered owner == the engine whose session
|
|
85
|
+
// lookupSession() returns.
|
|
86
|
+
class SessionOpGate {
|
|
87
|
+
public:
|
|
88
|
+
class OpScope {
|
|
89
|
+
public:
|
|
90
|
+
explicit OpScope(std::shared_ptr<SessionOpGate> gate)
|
|
91
|
+
: gate_(std::move(gate)) {
|
|
92
|
+
std::unique_lock<std::mutex> lock(gate_->mtx_);
|
|
93
|
+
gate_->cv_.wait(lock, [this] { return gate_->frozen_ == 0; });
|
|
94
|
+
++gate_->inflight_;
|
|
95
|
+
}
|
|
96
|
+
~OpScope() {
|
|
97
|
+
{
|
|
98
|
+
std::lock_guard<std::mutex> lock(gate_->mtx_);
|
|
99
|
+
--gate_->inflight_;
|
|
100
|
+
}
|
|
101
|
+
gate_->cv_.notify_all();
|
|
102
|
+
}
|
|
103
|
+
OpScope(const OpScope&) = delete;
|
|
104
|
+
OpScope& operator=(const OpScope&) = delete;
|
|
105
|
+
|
|
106
|
+
private:
|
|
107
|
+
std::shared_ptr<SessionOpGate> gate_;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
SessionOpGate() = default;
|
|
111
|
+
SessionOpGate(const SessionOpGate&) = delete;
|
|
112
|
+
SessionOpGate& operator=(const SessionOpGate&) = delete;
|
|
113
|
+
|
|
114
|
+
// Block new operations on this engine's sessions and wait for the in-flight
|
|
115
|
+
// ones to finish. Nests: the stop worker freezes on its thread and the wrap
|
|
116
|
+
// unfreezes later on the JS thread, and in between the destructor (if the
|
|
117
|
+
// wrap is finalized during environment teardown) freezes again around its
|
|
118
|
+
// own DestroyEngine().
|
|
119
|
+
void Freeze() {
|
|
120
|
+
std::unique_lock<std::mutex> lock(mtx_);
|
|
121
|
+
++frozen_;
|
|
122
|
+
cv_.wait(lock, [this] { return inflight_ == 0; });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Let operations run again. Pairs with Freeze().
|
|
126
|
+
void Unfreeze() {
|
|
127
|
+
{
|
|
128
|
+
std::lock_guard<std::mutex> lock(mtx_);
|
|
129
|
+
--frozen_;
|
|
130
|
+
}
|
|
131
|
+
cv_.notify_all();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- registry: SessionID -> owning engine's gate ---------------------------
|
|
135
|
+
// Each id maps to at most one gate at a time: the engine wraps refuse to
|
|
136
|
+
// construct an engine whose SessionID is already live in the process
|
|
137
|
+
// (ConfigError "Duplicate Session"). QuickFIX itself does NOT enforce that --
|
|
138
|
+
// its Session constructor ignores addSession()'s result, so a second engine
|
|
139
|
+
// would silently shadow the first in the process-wide session registry.
|
|
140
|
+
|
|
141
|
+
static void Register(const std::set<FIX::SessionID>& ids,
|
|
142
|
+
const std::shared_ptr<SessionOpGate>& gate) {
|
|
143
|
+
Registry& r = GetRegistry();
|
|
144
|
+
std::lock_guard<std::mutex> lock(r.mtx);
|
|
145
|
+
for (const FIX::SessionID& id : ids) r.gates[id] = gate;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove `ids` only where they still point at `gate`: a new engine may
|
|
149
|
+
// already have registered the same ids after this one unregistered them.
|
|
150
|
+
static void Unregister(const std::set<FIX::SessionID>& ids,
|
|
151
|
+
const SessionOpGate* gate) {
|
|
152
|
+
Registry& r = GetRegistry();
|
|
153
|
+
std::lock_guard<std::mutex> lock(r.mtx);
|
|
154
|
+
for (const FIX::SessionID& id : ids) {
|
|
155
|
+
auto it = r.gates.find(id);
|
|
156
|
+
if (it != r.gates.end() && it->second.get() == gate) r.gates.erase(it);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The gate of the engine owning `id`, or nullptr if no engine does.
|
|
161
|
+
static std::shared_ptr<SessionOpGate> Lookup(const FIX::SessionID& id) {
|
|
162
|
+
Registry& r = GetRegistry();
|
|
163
|
+
std::lock_guard<std::mutex> lock(r.mtx);
|
|
164
|
+
auto it = r.gates.find(id);
|
|
165
|
+
return it == r.gates.end() ? nullptr : it->second;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Hold an OpScope on `gate` -- the registered owner of `id` when the
|
|
169
|
+
// caller looked it up on the JS thread -- or return nullptr if it no longer
|
|
170
|
+
// is (-> SessionNotFound). Constructing the scope may block (the owner is
|
|
171
|
+
// stopping); once it is held the registry is re-read, and the scope is kept
|
|
172
|
+
// only if the same gate is still registered. Otherwise the owner was
|
|
173
|
+
// destroyed while we waited -- possibly replaced by a new engine with the
|
|
174
|
+
// same id -- and the operation is refused (nullptr) rather than run against
|
|
175
|
+
// the new owner, mirroring SessionWrap::Resolve().
|
|
176
|
+
static std::unique_ptr<OpScope> Acquire(
|
|
177
|
+
const std::shared_ptr<SessionOpGate>& gate, const FIX::SessionID& id) {
|
|
178
|
+
auto scope = std::make_unique<OpScope>(gate);
|
|
179
|
+
if (Lookup(id) != gate) return nullptr;
|
|
180
|
+
return scope;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private:
|
|
184
|
+
struct Registry {
|
|
185
|
+
std::mutex mtx;
|
|
186
|
+
std::map<FIX::SessionID, std::shared_ptr<SessionOpGate>> gates;
|
|
187
|
+
};
|
|
188
|
+
static Registry& GetRegistry() {
|
|
189
|
+
static Registry registry;
|
|
190
|
+
return registry;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::mutex mtx_;
|
|
194
|
+
std::condition_variable cv_;
|
|
195
|
+
int inflight_ = 0;
|
|
196
|
+
int frozen_ = 0;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
} // namespace napi_quickfix
|
|
200
|
+
|
|
201
|
+
#endif // NAPI_QUICKFIX_SESSION_OP_GATE_H
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#include "session_settings_wrap.h"
|
|
2
|
+
|
|
3
|
+
#include <set>
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
#include "errors.h"
|
|
8
|
+
#include "quickfix/SessionID.h"
|
|
9
|
+
#include "session_id_wrap.h"
|
|
10
|
+
|
|
11
|
+
namespace napi_quickfix {
|
|
12
|
+
|
|
13
|
+
Napi::FunctionReference SessionSettingsWrap::constructor_;
|
|
14
|
+
|
|
15
|
+
Napi::Object SessionSettingsWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
16
|
+
Napi::Function func = DefineClass(
|
|
17
|
+
env, "SessionSettings",
|
|
18
|
+
{
|
|
19
|
+
StaticMethod("fromString", &SessionSettingsWrap::FromString),
|
|
20
|
+
StaticMethod("fromFile", &SessionSettingsWrap::FromFile),
|
|
21
|
+
InstanceMethod("getSessions", &SessionSettingsWrap::GetSessions),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
constructor_ = Napi::Persistent(func);
|
|
25
|
+
constructor_.SuppressDestruct();
|
|
26
|
+
|
|
27
|
+
exports.Set("SessionSettingsWrap", func);
|
|
28
|
+
return exports;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
SessionSettingsWrap* SessionSettingsWrap::UnwrapArg(Napi::Env env,
|
|
32
|
+
Napi::Value value,
|
|
33
|
+
const char* argName) {
|
|
34
|
+
if (!value.IsObject() ||
|
|
35
|
+
!value.As<Napi::Object>().InstanceOf(constructor_.Value())) {
|
|
36
|
+
throw Napi::TypeError::New(
|
|
37
|
+
env, std::string(argName) + " must be a SessionSettings instance");
|
|
38
|
+
}
|
|
39
|
+
return Napi::ObjectWrap<SessionSettingsWrap>::Unwrap(
|
|
40
|
+
value.As<Napi::Object>());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Constructor is internal: (kind: "string"|"file", payload: string).
|
|
44
|
+
// Users go through the static fromString/fromFile factories.
|
|
45
|
+
SessionSettingsWrap::SessionSettingsWrap(const Napi::CallbackInfo& info)
|
|
46
|
+
: Napi::ObjectWrap<SessionSettingsWrap>(info) {
|
|
47
|
+
Napi::Env env = info.Env();
|
|
48
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
49
|
+
throw Napi::TypeError::New(
|
|
50
|
+
env,
|
|
51
|
+
"SessionSettings cannot be constructed directly; use "
|
|
52
|
+
"SessionSettings.fromString() or SessionSettings.fromFile()");
|
|
53
|
+
}
|
|
54
|
+
std::string kind = info[0].As<Napi::String>().Utf8Value();
|
|
55
|
+
std::string payload = info[1].As<Napi::String>().Utf8Value();
|
|
56
|
+
|
|
57
|
+
NQ_TRY(env) {
|
|
58
|
+
if (kind == "string") {
|
|
59
|
+
std::istringstream stream(payload);
|
|
60
|
+
settings_ = FIX::SessionSettings(stream);
|
|
61
|
+
} else { // "file"
|
|
62
|
+
settings_ = FIX::SessionSettings(payload);
|
|
63
|
+
}
|
|
64
|
+
} NQ_CATCH(env)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Napi::Value SessionSettingsWrap::FromString(const Napi::CallbackInfo& info) {
|
|
68
|
+
Napi::Env env = info.Env();
|
|
69
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
70
|
+
throw Napi::TypeError::New(env, "fromString(text: string)");
|
|
71
|
+
}
|
|
72
|
+
return constructor_.New(
|
|
73
|
+
{Napi::String::New(env, "string"), info[0].As<Napi::String>()});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Napi::Value SessionSettingsWrap::FromFile(const Napi::CallbackInfo& info) {
|
|
77
|
+
Napi::Env env = info.Env();
|
|
78
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
79
|
+
throw Napi::TypeError::New(env, "fromFile(path: string)");
|
|
80
|
+
}
|
|
81
|
+
return constructor_.New(
|
|
82
|
+
{Napi::String::New(env, "file"), info[0].As<Napi::String>()});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Napi::Value SessionSettingsWrap::GetSessions(const Napi::CallbackInfo& info) {
|
|
86
|
+
Napi::Env env = info.Env();
|
|
87
|
+
NQ_TRY(env) {
|
|
88
|
+
std::set<FIX::SessionID> sessions = settings_.getSessions();
|
|
89
|
+
Napi::Array arr = Napi::Array::New(env, sessions.size());
|
|
90
|
+
uint32_t i = 0;
|
|
91
|
+
for (const auto& id : sessions) {
|
|
92
|
+
arr.Set(i++, SessionIDWrap::NewInstance(env, id));
|
|
93
|
+
}
|
|
94
|
+
return arr;
|
|
95
|
+
} NQ_CATCH(env)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} // namespace napi_quickfix
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
|
|
2
|
+
#define NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include "quickfix/SessionSettings.h"
|
|
7
|
+
|
|
8
|
+
namespace napi_quickfix {
|
|
9
|
+
|
|
10
|
+
// Napi::ObjectWrap over FIX::SessionSettings.
|
|
11
|
+
//
|
|
12
|
+
// SessionSettings.fromString(text): SessionSettings
|
|
13
|
+
// SessionSettings.fromFile(path): SessionSettings
|
|
14
|
+
// settings.getSessions(): SessionID[]
|
|
15
|
+
class SessionSettingsWrap : public Napi::ObjectWrap<SessionSettingsWrap> {
|
|
16
|
+
public:
|
|
17
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
18
|
+
|
|
19
|
+
explicit SessionSettingsWrap(const Napi::CallbackInfo& info);
|
|
20
|
+
|
|
21
|
+
FIX::SessionSettings& Settings() { return settings_; }
|
|
22
|
+
const FIX::SessionSettings& Settings() const { return settings_; }
|
|
23
|
+
|
|
24
|
+
static SessionSettingsWrap* UnwrapArg(Napi::Env env, Napi::Value value,
|
|
25
|
+
const char* argName);
|
|
26
|
+
|
|
27
|
+
private:
|
|
28
|
+
static Napi::FunctionReference constructor_;
|
|
29
|
+
|
|
30
|
+
static Napi::Value FromString(const Napi::CallbackInfo& info);
|
|
31
|
+
static Napi::Value FromFile(const Napi::CallbackInfo& info);
|
|
32
|
+
|
|
33
|
+
Napi::Value GetSessions(const Napi::CallbackInfo& info);
|
|
34
|
+
|
|
35
|
+
FIX::SessionSettings settings_;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
} // namespace napi_quickfix
|
|
39
|
+
|
|
40
|
+
#endif // NAPI_QUICKFIX_SESSION_SETTINGS_WRAP_H
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
|
|
3
|
+
#include <memory>
|
|
4
|
+
#include <set>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <utility>
|
|
7
|
+
|
|
8
|
+
#include "errors.h"
|
|
9
|
+
#include "field_map_ops.h"
|
|
10
|
+
#include "message_wrap.h"
|
|
11
|
+
#include "quickfix/Exceptions.h"
|
|
12
|
+
#include "quickfix/Message.h"
|
|
13
|
+
#include "quickfix/Session.h"
|
|
14
|
+
#include "quickfix/SessionID.h"
|
|
15
|
+
#include "session_id_wrap.h"
|
|
16
|
+
#include "session_op_gate.h"
|
|
17
|
+
#include "session_wrap.h"
|
|
18
|
+
|
|
19
|
+
namespace napi_quickfix {
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
// AsyncWorker for FIX::Session::sendToTarget. Runs OFF the JS main thread so the
|
|
24
|
+
// event loop stays free to service the ApplicationBridge TSFN BlockingCall(s)
|
|
25
|
+
// that toApp/toAdmin trigger during the send. Running on the main thread would
|
|
26
|
+
// deadlock (main blocked in sendToTarget -> toApp -> BlockingCall -> waits for
|
|
27
|
+
// main to run CallJs, which it can't).
|
|
28
|
+
//
|
|
29
|
+
// The FIX::Message (a fieldmap::CloneMessage, nested group entries kept
|
|
30
|
+
// typed) and FIX::SessionID are COPIED on the JS thread before Queue()ing, so
|
|
31
|
+
// Execute() never touches any JS object. toApp may mutate the worker's private
|
|
32
|
+
// copy during send — that's fine, it's QuickFIX's business.
|
|
33
|
+
//
|
|
34
|
+
// Two upstream overloads are supported: by explicit SessionID, or by qualifier
|
|
35
|
+
// (QuickFIX then resolves the session from the message's own header fields:
|
|
36
|
+
// BeginString / SenderCompID / TargetCompID).
|
|
37
|
+
//
|
|
38
|
+
// Session::sendToTarget looks the session up in the process-wide registry and
|
|
39
|
+
// uses the raw pointer, so Execute() runs under the owning engine's
|
|
40
|
+
// SessionOpGate::OpScope to keep that engine alive until the send has returned.
|
|
41
|
+
// That gate is resolved by SendToTarget() on the JS thread, before the worker
|
|
42
|
+
// is queued (for the qualifier form after deriving the SessionID from the
|
|
43
|
+
// message header, exactly as the upstream overload does): the send is thereby
|
|
44
|
+
// bound to the engine that owned the session when it was called, and can
|
|
45
|
+
// never run through an engine registered for the same id afterwards -- not
|
|
46
|
+
// even if it sat in the libuv queue across the owner's stop() and that
|
|
47
|
+
// engine's construction. Execute() only re-checks that the gate is still the
|
|
48
|
+
// owner once the scope is held (see session_op_gate.h).
|
|
49
|
+
class SendToTargetWorker : public Napi::AsyncWorker {
|
|
50
|
+
public:
|
|
51
|
+
SendToTargetWorker(Napi::Env env, FIX::Message message, FIX::SessionID id,
|
|
52
|
+
std::shared_ptr<SessionOpGate> gate)
|
|
53
|
+
: Napi::AsyncWorker(env),
|
|
54
|
+
deferred_(Napi::Promise::Deferred::New(env)),
|
|
55
|
+
message_(std::move(message)),
|
|
56
|
+
id_(std::move(id)),
|
|
57
|
+
gate_(std::move(gate)) {}
|
|
58
|
+
|
|
59
|
+
Napi::Promise Promise() { return deferred_.Promise(); }
|
|
60
|
+
|
|
61
|
+
// Worker thread. NO Napi/JS access.
|
|
62
|
+
void Execute() override {
|
|
63
|
+
try {
|
|
64
|
+
std::unique_ptr<SessionOpGate::OpScope> inflight =
|
|
65
|
+
SessionOpGate::Acquire(gate_, id_);
|
|
66
|
+
if (!inflight) {
|
|
67
|
+
throw FIX::SessionNotFound(id_.toString());
|
|
68
|
+
}
|
|
69
|
+
result_ = FIX::Session::sendToTarget(message_, id_);
|
|
70
|
+
} catch (const std::exception& e) {
|
|
71
|
+
err_.Capture(e);
|
|
72
|
+
} catch (...) {
|
|
73
|
+
err_.has = true;
|
|
74
|
+
err_.message = "unknown native error";
|
|
75
|
+
err_.fixErrorName = "Error";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void OnOK() override {
|
|
80
|
+
Napi::Env env = Env();
|
|
81
|
+
Napi::HandleScope scope(env);
|
|
82
|
+
if (err_.has) {
|
|
83
|
+
deferred_.Reject(err_.ToError(env).Value());
|
|
84
|
+
} else {
|
|
85
|
+
deferred_.Resolve(Napi::Boolean::New(env, result_));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
void OnError(const Napi::Error& e) override {
|
|
90
|
+
Napi::HandleScope scope(Env());
|
|
91
|
+
deferred_.Reject(e.Value());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private:
|
|
95
|
+
Napi::Promise::Deferred deferred_;
|
|
96
|
+
FIX::Message message_;
|
|
97
|
+
FIX::SessionID id_;
|
|
98
|
+
std::shared_ptr<SessionOpGate> gate_;
|
|
99
|
+
bool result_ = false;
|
|
100
|
+
CapturedError err_;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// sendToTarget(message: Message, sessionID: SessionID): Promise<boolean>
|
|
104
|
+
// sendToTarget(message: Message, qualifier?: string): Promise<boolean>
|
|
105
|
+
Napi::Value SendToTarget(const Napi::CallbackInfo& info) {
|
|
106
|
+
Napi::Env env = info.Env();
|
|
107
|
+
MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
|
|
108
|
+
|
|
109
|
+
// Copy the message (and session id) by value on the JS thread so the worker
|
|
110
|
+
// never touches JS-owned objects.
|
|
111
|
+
FIX::Message message = fieldmap::CloneMessage(msg->Message());
|
|
112
|
+
const bool byQualifier = info.Length() < 2 || info[1].IsUndefined() ||
|
|
113
|
+
info[1].IsString();
|
|
114
|
+
FIX::SessionID id;
|
|
115
|
+
if (!byQualifier) {
|
|
116
|
+
id = SessionIDWrap::UnwrapArg(env, info[1], "sessionID")->SessionID();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resolve the owning engine HERE, on the JS thread (see the worker's
|
|
120
|
+
// comment): a session that no engine owns is refused right away, with the
|
|
121
|
+
// same error the worker reports when the engine goes away under it.
|
|
122
|
+
std::shared_ptr<SessionOpGate> gate;
|
|
123
|
+
try {
|
|
124
|
+
if (byQualifier) {
|
|
125
|
+
// Mirrors FIX::Session::sendToTarget(Message&, const std::string&):
|
|
126
|
+
// a header missing tag 8/49/56 is a SessionNotFound.
|
|
127
|
+
const std::string qualifier =
|
|
128
|
+
info[1].IsString() ? info[1].As<Napi::String>().Utf8Value()
|
|
129
|
+
: std::string();
|
|
130
|
+
try {
|
|
131
|
+
id = message.getSessionID(qualifier);
|
|
132
|
+
} catch (const FIX::FieldNotFound&) {
|
|
133
|
+
throw FIX::SessionNotFound();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
gate = SessionOpGate::Lookup(id);
|
|
137
|
+
if (!gate) {
|
|
138
|
+
throw FIX::SessionNotFound(id.toString());
|
|
139
|
+
}
|
|
140
|
+
} catch (const FIX::Exception& e) {
|
|
141
|
+
CapturedError err;
|
|
142
|
+
err.Capture(e);
|
|
143
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
144
|
+
deferred.Reject(err.ToError(env).Value());
|
|
145
|
+
return deferred.Promise();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
auto* worker = new SendToTargetWorker(env, std::move(message), std::move(id),
|
|
149
|
+
std::move(gate));
|
|
150
|
+
Napi::Promise promise = worker->Promise();
|
|
151
|
+
worker->Queue();
|
|
152
|
+
return promise;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// lookupSession(sessionID: SessionID): Session | undefined
|
|
156
|
+
Napi::Value LookupSession(const Napi::CallbackInfo& info) {
|
|
157
|
+
Napi::Env env = info.Env();
|
|
158
|
+
SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[0], "sessionID");
|
|
159
|
+
NQ_TRY(env) {
|
|
160
|
+
// The gate of the engine owning the id; registered by every wrap
|
|
161
|
+
// constructor right after it created its sessions, unregistered right
|
|
162
|
+
// before they are destroyed, so (both on the JS thread) a session that
|
|
163
|
+
// exists has exactly one. No gate means no session.
|
|
164
|
+
std::shared_ptr<SessionOpGate> gate = SessionOpGate::Lookup(id->SessionID());
|
|
165
|
+
if (!gate || !FIX::Session::doesSessionExist(id->SessionID())) {
|
|
166
|
+
return env.Undefined();
|
|
167
|
+
}
|
|
168
|
+
return SessionWrap::NewInstance(env, id->SessionID(), std::move(gate));
|
|
169
|
+
} NQ_CATCH(env)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// doesSessionExist(sessionID: SessionID): boolean
|
|
173
|
+
Napi::Value DoesSessionExist(const Napi::CallbackInfo& info) {
|
|
174
|
+
Napi::Env env = info.Env();
|
|
175
|
+
SessionIDWrap* id = SessionIDWrap::UnwrapArg(env, info[0], "sessionID");
|
|
176
|
+
NQ_TRY(env) {
|
|
177
|
+
return Napi::Boolean::New(env,
|
|
178
|
+
FIX::Session::doesSessionExist(id->SessionID()));
|
|
179
|
+
} NQ_CATCH(env)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// getSessions(): SessionID[] -- every session registered in this process.
|
|
183
|
+
Napi::Value GetSessions(const Napi::CallbackInfo& info) {
|
|
184
|
+
Napi::Env env = info.Env();
|
|
185
|
+
NQ_TRY(env) {
|
|
186
|
+
const std::set<FIX::SessionID> ids = FIX::Session::getSessions();
|
|
187
|
+
Napi::Array out = Napi::Array::New(env, ids.size());
|
|
188
|
+
uint32_t i = 0;
|
|
189
|
+
for (const FIX::SessionID& id : ids) {
|
|
190
|
+
out.Set(i++, SessionIDWrap::NewInstance(env, id));
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
} NQ_CATCH(env)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// numSessions(): number
|
|
197
|
+
Napi::Value NumSessions(const Napi::CallbackInfo& info) {
|
|
198
|
+
Napi::Env env = info.Env();
|
|
199
|
+
NQ_TRY(env) {
|
|
200
|
+
return Napi::Number::New(env,
|
|
201
|
+
static_cast<double>(FIX::Session::numSessions()));
|
|
202
|
+
} NQ_CATCH(env)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
} // namespace
|
|
206
|
+
|
|
207
|
+
void RegisterSessionStatic(Napi::Env env, Napi::Object exports) {
|
|
208
|
+
exports.Set("sendToTarget",
|
|
209
|
+
Napi::Function::New(env, SendToTarget, "sendToTarget"));
|
|
210
|
+
exports.Set("lookupSession",
|
|
211
|
+
Napi::Function::New(env, LookupSession, "lookupSession"));
|
|
212
|
+
exports.Set("doesSessionExist",
|
|
213
|
+
Napi::Function::New(env, DoesSessionExist, "doesSessionExist"));
|
|
214
|
+
exports.Set("getSessions",
|
|
215
|
+
Napi::Function::New(env, GetSessions, "getSessions"));
|
|
216
|
+
exports.Set("numSessions",
|
|
217
|
+
Napi::Function::New(env, NumSessions, "numSessions"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} // namespace napi_quickfix
|