@homaiohq/napi-quickfix 0.1.0 → 0.3.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 +5 -1
- package/README.md +344 -18
- package/cpp/acceptor_wrap.cpp +253 -19
- package/cpp/acceptor_wrap.h +60 -1
- package/cpp/addon.cpp +10 -6
- package/cpp/application_bridge.cpp +114 -30
- package/cpp/application_bridge.h +57 -4
- package/cpp/data_dictionary_wrap.cpp +89 -3
- package/cpp/data_dictionary_wrap.h +27 -5
- package/cpp/engine_common.h +108 -0
- package/cpp/engine_workers.h +99 -18
- 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 +262 -25
- package/cpp/initiator_wrap.h +37 -2
- package/cpp/message_wrap.cpp +171 -78
- package/cpp/message_wrap.h +47 -5
- package/cpp/session_op_gate.h +201 -0
- package/cpp/session_static.cpp +139 -12
- package/cpp/session_wrap.cpp +438 -0
- package/cpp/session_wrap.h +144 -0
- package/dist/cjs/acceptor.d.ts +0 -1
- package/dist/cjs/acceptor.js +0 -1
- package/dist/cjs/application.d.ts +0 -1
- package/dist/cjs/application.js +0 -1
- package/dist/cjs/data-dictionary.d.ts +21 -3
- package/dist/cjs/data-dictionary.js +32 -4
- package/dist/cjs/engine.d.ts +42 -4
- package/dist/cjs/engine.js +50 -5
- package/dist/cjs/enums.d.ts +48 -14
- package/dist/cjs/enums.js +20 -25
- package/dist/cjs/field-value.d.ts +12 -0
- package/dist/cjs/field-value.js +40 -0
- package/dist/cjs/generated/fields.d.ts +0 -1
- package/dist/cjs/generated/fields.js +0 -1
- 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 +28 -5
- package/dist/cjs/index.js +30 -28
- package/dist/cjs/initiator.d.ts +0 -1
- package/dist/cjs/initiator.js +0 -1
- package/dist/cjs/message.d.ts +129 -5
- package/dist/cjs/message.js +146 -9
- package/dist/cjs/native.d.ts +179 -17
- package/dist/cjs/native.js +0 -1
- package/dist/cjs/session-id.d.ts +12 -1
- package/dist/cjs/session-id.js +21 -1
- package/dist/cjs/session-settings.d.ts +0 -1
- package/dist/cjs/session-settings.js +0 -1
- package/dist/cjs/session.d.ts +213 -0
- package/dist/cjs/session.js +337 -0
- package/dist/cjs/tls-tunnel.d.ts +143 -0
- package/dist/cjs/tls-tunnel.js +261 -0
- package/dist/esm/acceptor.d.ts +0 -1
- package/dist/esm/acceptor.js +0 -1
- package/dist/esm/application.d.ts +0 -1
- package/dist/esm/application.js +0 -1
- package/dist/esm/data-dictionary.d.ts +21 -3
- package/dist/esm/data-dictionary.js +32 -4
- package/dist/esm/engine.d.ts +42 -4
- package/dist/esm/engine.js +51 -6
- package/dist/esm/enums.d.ts +48 -14
- package/dist/esm/enums.js +14 -24
- package/dist/esm/field-value.d.ts +12 -0
- package/dist/esm/field-value.js +37 -0
- package/dist/esm/generated/fields.d.ts +0 -1
- package/dist/esm/generated/fields.js +0 -1
- 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 +28 -5
- package/dist/esm/index.js +18 -26
- package/dist/esm/initiator.d.ts +0 -1
- package/dist/esm/initiator.js +0 -1
- package/dist/esm/message.d.ts +129 -5
- package/dist/esm/message.js +146 -9
- package/dist/esm/native.d.ts +179 -17
- package/dist/esm/native.js +0 -1
- package/dist/esm/session-id.d.ts +12 -1
- package/dist/esm/session-id.js +20 -1
- package/dist/esm/session-settings.d.ts +0 -1
- package/dist/esm/session-settings.js +0 -1
- package/dist/esm/session.d.ts +213 -0
- package/dist/esm/session.js +329 -0
- package/dist/esm/tls-tunnel.d.ts +143 -0
- package/dist/esm/tls-tunnel.js +256 -0
- package/package.json +13 -1
- 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/cpp/enums.cpp +0 -66
- package/dist/cjs/acceptor.d.ts.map +0 -1
- package/dist/cjs/acceptor.js.map +0 -1
- package/dist/cjs/application.d.ts.map +0 -1
- package/dist/cjs/application.js.map +0 -1
- package/dist/cjs/data-dictionary.d.ts.map +0 -1
- package/dist/cjs/data-dictionary.js.map +0 -1
- package/dist/cjs/engine.d.ts.map +0 -1
- package/dist/cjs/engine.js.map +0 -1
- package/dist/cjs/enums.d.ts.map +0 -1
- package/dist/cjs/enums.js.map +0 -1
- package/dist/cjs/generated/fields.d.ts.map +0 -1
- package/dist/cjs/generated/fields.js.map +0 -1
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/initiator.d.ts.map +0 -1
- package/dist/cjs/initiator.js.map +0 -1
- package/dist/cjs/message.d.ts.map +0 -1
- package/dist/cjs/message.js.map +0 -1
- package/dist/cjs/native.d.ts.map +0 -1
- package/dist/cjs/native.js.map +0 -1
- package/dist/cjs/session-id.d.ts.map +0 -1
- package/dist/cjs/session-id.js.map +0 -1
- package/dist/cjs/session-settings.d.ts.map +0 -1
- package/dist/cjs/session-settings.js.map +0 -1
- package/dist/esm/acceptor.d.ts.map +0 -1
- package/dist/esm/acceptor.js.map +0 -1
- package/dist/esm/application.d.ts.map +0 -1
- package/dist/esm/application.js.map +0 -1
- package/dist/esm/data-dictionary.d.ts.map +0 -1
- package/dist/esm/data-dictionary.js.map +0 -1
- package/dist/esm/engine.d.ts.map +0 -1
- package/dist/esm/engine.js.map +0 -1
- package/dist/esm/enums.d.ts.map +0 -1
- package/dist/esm/enums.js.map +0 -1
- package/dist/esm/generated/fields.d.ts.map +0 -1
- package/dist/esm/generated/fields.js.map +0 -1
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/initiator.d.ts.map +0 -1
- package/dist/esm/initiator.js.map +0 -1
- package/dist/esm/message.d.ts.map +0 -1
- package/dist/esm/message.js.map +0 -1
- package/dist/esm/native.d.ts.map +0 -1
- package/dist/esm/native.js.map +0 -1
- package/dist/esm/session-id.d.ts.map +0 -1
- package/dist/esm/session-id.js.map +0 -1
- package/dist/esm/session-settings.d.ts.map +0 -1
- package/dist/esm/session-settings.js.map +0 -1
package/cpp/message_wrap.cpp
CHANGED
|
@@ -2,52 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <string>
|
|
5
|
+
#include <utility>
|
|
5
6
|
|
|
7
|
+
#include "data_dictionary_wrap.h"
|
|
6
8
|
#include "errors.h"
|
|
9
|
+
#include "field_map_ops.h"
|
|
7
10
|
#include "quickfix/FieldNumbers.h"
|
|
8
11
|
|
|
9
12
|
namespace napi_quickfix {
|
|
10
13
|
|
|
11
14
|
Napi::FunctionReference MessageWrap::constructor_;
|
|
12
15
|
|
|
13
|
-
namespace {
|
|
14
|
-
|
|
15
|
-
// Coerce a JS arg to a FIX field value string. Accepts string or number
|
|
16
|
-
// (numbers are stringified without trailing ".0"). Throws TypeError otherwise.
|
|
17
|
-
std::string CoerceValue(Napi::Env env, Napi::Value v) {
|
|
18
|
-
if (v.IsString()) {
|
|
19
|
-
return v.As<Napi::String>().Utf8Value();
|
|
20
|
-
}
|
|
21
|
-
if (v.IsNumber()) {
|
|
22
|
-
double d = v.As<Napi::Number>().DoubleValue();
|
|
23
|
-
// Integers -> plain integer string; otherwise a plain double string.
|
|
24
|
-
if (d == static_cast<double>(static_cast<long long>(d))) {
|
|
25
|
-
return std::to_string(static_cast<long long>(d));
|
|
26
|
-
}
|
|
27
|
-
return std::to_string(d);
|
|
28
|
-
}
|
|
29
|
-
throw Napi::TypeError::New(env, "field value must be a string or number");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
int CoerceTag(Napi::Env env, Napi::Value v) {
|
|
33
|
-
if (!v.IsNumber()) {
|
|
34
|
-
throw Napi::TypeError::New(env, "field tag must be a number");
|
|
35
|
-
}
|
|
36
|
-
return v.As<Napi::Number>().Int32Value();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
} // namespace
|
|
40
|
-
|
|
41
16
|
Napi::Object MessageWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
42
17
|
Napi::Function func = DefineClass(
|
|
43
18
|
env, "Message",
|
|
44
19
|
{
|
|
45
20
|
InstanceMethod("getField", &MessageWrap::GetField),
|
|
46
21
|
InstanceMethod("setField", &MessageWrap::SetField),
|
|
22
|
+
InstanceMethod("isSetField", &MessageWrap::IsSetField),
|
|
23
|
+
InstanceMethod("removeField", &MessageWrap::RemoveField),
|
|
24
|
+
InstanceMethod("getFieldIfSet", &MessageWrap::GetFieldIfSet),
|
|
47
25
|
InstanceMethod("getHeaderField", &MessageWrap::GetHeaderField),
|
|
48
26
|
InstanceMethod("setHeaderField", &MessageWrap::SetHeaderField),
|
|
49
27
|
InstanceMethod("getTrailerField", &MessageWrap::GetTrailerField),
|
|
50
28
|
InstanceMethod("setTrailerField", &MessageWrap::SetTrailerField),
|
|
29
|
+
InstanceMethod("isEmpty", &MessageWrap::IsEmpty),
|
|
30
|
+
InstanceMethod("totalFields", &MessageWrap::TotalFields),
|
|
31
|
+
InstanceMethod("clear", &MessageWrap::Clear),
|
|
32
|
+
InstanceMethod("fields", &MessageWrap::Fields),
|
|
33
|
+
InstanceMethod("headerFields", &MessageWrap::HeaderFields),
|
|
34
|
+
InstanceMethod("trailerFields", &MessageWrap::TrailerFields),
|
|
35
|
+
InstanceMethod("addGroup", &MessageWrap::AddGroup),
|
|
36
|
+
InstanceMethod("getGroup", &MessageWrap::GetGroup),
|
|
37
|
+
InstanceMethod("replaceGroup", &MessageWrap::ReplaceGroup),
|
|
38
|
+
InstanceMethod("removeGroup", &MessageWrap::RemoveGroup),
|
|
39
|
+
InstanceMethod("hasGroup", &MessageWrap::HasGroup),
|
|
40
|
+
InstanceMethod("groupCount", &MessageWrap::GroupCount),
|
|
51
41
|
InstanceMethod("toString", &MessageWrap::ToString),
|
|
52
42
|
InstanceMethod("toPretty", &MessageWrap::ToPretty),
|
|
53
43
|
InstanceMethod("getMsgType", &MessageWrap::GetMsgType),
|
|
@@ -60,11 +50,23 @@ Napi::Object MessageWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
|
60
50
|
return exports;
|
|
61
51
|
}
|
|
62
52
|
|
|
53
|
+
// Both construct an empty MessageWrap then fill its (empty) FIX::Message. A
|
|
54
|
+
// move keeps the nested entries typed by itself: FieldMap's move assignment
|
|
55
|
+
// takes the entry pointers over.
|
|
63
56
|
Napi::Object MessageWrap::NewInstance(Napi::Env env, const FIX::Message& msg) {
|
|
64
|
-
// Construct an empty MessageWrap then overwrite its FIX::Message with a copy.
|
|
65
57
|
Napi::Object obj = constructor_.New({});
|
|
66
58
|
MessageWrap* wrap = Napi::ObjectWrap<MessageWrap>::Unwrap(obj);
|
|
67
|
-
wrap->message_
|
|
59
|
+
fieldmap::CloneMessage(wrap->message_, msg);
|
|
60
|
+
return obj;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Napi::Object MessageWrap::NewInstance(
|
|
64
|
+
Napi::Env env, FIX::Message&& msg,
|
|
65
|
+
std::shared_ptr<const FIX::DataDictionary> sessionDictionary) {
|
|
66
|
+
Napi::Object obj = constructor_.New({});
|
|
67
|
+
MessageWrap* wrap = Napi::ObjectWrap<MessageWrap>::Unwrap(obj);
|
|
68
|
+
wrap->message_ = std::move(msg);
|
|
69
|
+
wrap->sessionDictionary_ = std::move(sessionDictionary);
|
|
68
70
|
return obj;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -87,87 +89,178 @@ MessageWrap::MessageWrap(const Napi::CallbackInfo& info)
|
|
|
87
89
|
}
|
|
88
90
|
if (!info[0].IsString()) {
|
|
89
91
|
throw Napi::TypeError::New(
|
|
90
|
-
env,
|
|
92
|
+
env,
|
|
93
|
+
"new Message(raw: string, validate?: boolean, "
|
|
94
|
+
"sessionDictionary?: DataDictionary, "
|
|
95
|
+
"applicationDictionary?: DataDictionary)");
|
|
91
96
|
}
|
|
92
97
|
const std::string raw = info[0].As<Napi::String>().Utf8Value();
|
|
93
98
|
bool validate = false;
|
|
94
|
-
if (info.Length() >= 2 && !
|
|
99
|
+
if (info.Length() >= 2 && !fieldmap::IsNullish(info[1])) {
|
|
95
100
|
validate = info[1].ToBoolean().Value();
|
|
96
101
|
}
|
|
102
|
+
const FIX::DataDictionary* sessionDD = nullptr;
|
|
103
|
+
const FIX::DataDictionary* appDD = nullptr;
|
|
104
|
+
if (info.Length() >= 3 && !fieldmap::IsNullish(info[2])) {
|
|
105
|
+
sessionDictionary_ =
|
|
106
|
+
DataDictionaryWrap::UnwrapArg(env, info[2], "sessionDictionary")
|
|
107
|
+
->Shared();
|
|
108
|
+
sessionDD = sessionDictionary_.get();
|
|
109
|
+
}
|
|
110
|
+
if (info.Length() >= 4 && !fieldmap::IsNullish(info[3])) {
|
|
111
|
+
appDD = &DataDictionaryWrap::UnwrapArg(env, info[3], "applicationDictionary")
|
|
112
|
+
->Dictionary();
|
|
113
|
+
}
|
|
97
114
|
NQ_TRY(env) {
|
|
98
|
-
|
|
115
|
+
if (sessionDD != nullptr || appDD != nullptr) {
|
|
116
|
+
// The dictionaries are read only during this call; the wrapper objects
|
|
117
|
+
// are kept alive by the caller's arguments for its duration.
|
|
118
|
+
message_.setString(raw, validate, sessionDD, appDD);
|
|
119
|
+
} else {
|
|
120
|
+
message_.setString(raw, validate);
|
|
121
|
+
}
|
|
99
122
|
} NQ_CATCH(env)
|
|
100
123
|
}
|
|
101
124
|
|
|
125
|
+
namespace {
|
|
126
|
+
|
|
127
|
+
bool Holds(const FIX::FieldMap& map, int tag) {
|
|
128
|
+
return map.isSetField(tag) || map.hasGroup(tag);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
} // namespace
|
|
132
|
+
|
|
133
|
+
// Auto-route standard header/trailer fields (MsgType, SenderCompID, CheckSum,
|
|
134
|
+
// NoHops, ...) and the ones the session dictionary declares to the right
|
|
135
|
+
// section, as the parser does, so callers don't have to know which section a
|
|
136
|
+
// tag belongs to, and a dictionary header field that is removed and set again
|
|
137
|
+
// goes back to the header. Without a dictionary (a message built in JS or
|
|
138
|
+
// parsed without one), any other tag is a body field unless the header or
|
|
139
|
+
// trailer already holds it, so reads find such a field and setField /
|
|
140
|
+
// removeField edit it in place rather than adding a body duplicate. A tag
|
|
141
|
+
// held nowhere stays a body field.
|
|
142
|
+
FIX::FieldMap& MessageWrap::SectionFor(int tag) {
|
|
143
|
+
const FIX::DataDictionary* dd = sessionDictionary_.get();
|
|
144
|
+
if (FIX::Message::isHeaderField(tag, dd)) {
|
|
145
|
+
return message_.getHeader();
|
|
146
|
+
}
|
|
147
|
+
if (FIX::Message::isTrailerField(tag, dd)) {
|
|
148
|
+
return message_.getTrailer();
|
|
149
|
+
}
|
|
150
|
+
if (Holds(message_, tag)) {
|
|
151
|
+
return message_;
|
|
152
|
+
}
|
|
153
|
+
if (Holds(message_.getHeader(), tag)) {
|
|
154
|
+
return message_.getHeader();
|
|
155
|
+
}
|
|
156
|
+
if (Holds(message_.getTrailer(), tag)) {
|
|
157
|
+
return message_.getTrailer();
|
|
158
|
+
}
|
|
159
|
+
return message_;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#define NQ_ROUTED [this](int tag) -> FIX::FieldMap& { return SectionFor(tag); }
|
|
163
|
+
#define NQ_HEADER [this](int) -> FIX::FieldMap& { return message_.getHeader(); }
|
|
164
|
+
#define NQ_TRAILER [this](int) -> FIX::FieldMap& { return message_.getTrailer(); }
|
|
165
|
+
|
|
102
166
|
Napi::Value MessageWrap::GetField(const Napi::CallbackInfo& info) {
|
|
103
|
-
|
|
104
|
-
int tag = CoerceTag(env, info[0]);
|
|
105
|
-
NQ_TRY(env) {
|
|
106
|
-
// Auto-route standard header/trailer fields (MsgType, SenderCompID,
|
|
107
|
-
// CheckSum, ...) to the right section so callers don't have to know which
|
|
108
|
-
// section a well-known tag belongs to. Everything else is a body field.
|
|
109
|
-
const std::string& value =
|
|
110
|
-
FIX::Message::isHeaderField(tag) ? message_.getHeader().getField(tag)
|
|
111
|
-
: FIX::Message::isTrailerField(tag) ? message_.getTrailer().getField(tag)
|
|
112
|
-
: message_.getField(tag);
|
|
113
|
-
return Napi::String::New(env, value);
|
|
114
|
-
} NQ_CATCH(env)
|
|
167
|
+
return fieldmap::GetField(info, NQ_ROUTED);
|
|
115
168
|
}
|
|
116
169
|
|
|
117
170
|
Napi::Value MessageWrap::SetField(const Napi::CallbackInfo& info) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return
|
|
171
|
+
return fieldmap::SetField(info, NQ_ROUTED);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Napi::Value MessageWrap::IsSetField(const Napi::CallbackInfo& info) {
|
|
175
|
+
return fieldmap::IsSetField(info, NQ_ROUTED);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Napi::Value MessageWrap::RemoveField(const Napi::CallbackInfo& info) {
|
|
179
|
+
return fieldmap::RemoveField(info, NQ_ROUTED);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Napi::Value MessageWrap::GetFieldIfSet(const Napi::CallbackInfo& info) {
|
|
183
|
+
return fieldmap::GetFieldIfSet(info, NQ_ROUTED);
|
|
131
184
|
}
|
|
132
185
|
|
|
133
186
|
Napi::Value MessageWrap::GetHeaderField(const Napi::CallbackInfo& info) {
|
|
134
|
-
|
|
135
|
-
int tag = CoerceTag(env, info[0]);
|
|
136
|
-
NQ_TRY(env) {
|
|
137
|
-
const std::string& value = message_.getHeader().getField(tag);
|
|
138
|
-
return Napi::String::New(env, value);
|
|
139
|
-
} NQ_CATCH(env)
|
|
187
|
+
return fieldmap::GetField(info, NQ_HEADER);
|
|
140
188
|
}
|
|
141
189
|
|
|
142
190
|
Napi::Value MessageWrap::SetHeaderField(const Napi::CallbackInfo& info) {
|
|
143
|
-
|
|
144
|
-
int tag = CoerceTag(env, info[0]);
|
|
145
|
-
std::string value = CoerceValue(env, info[1]);
|
|
146
|
-
NQ_TRY(env) {
|
|
147
|
-
message_.getHeader().setField(tag, value);
|
|
148
|
-
} NQ_CATCH(env)
|
|
149
|
-
return env.Undefined();
|
|
191
|
+
return fieldmap::SetField(info, NQ_HEADER);
|
|
150
192
|
}
|
|
151
193
|
|
|
152
194
|
Napi::Value MessageWrap::GetTrailerField(const Napi::CallbackInfo& info) {
|
|
195
|
+
return fieldmap::GetField(info, NQ_TRAILER);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Napi::Value MessageWrap::SetTrailerField(const Napi::CallbackInfo& info) {
|
|
199
|
+
return fieldmap::SetField(info, NQ_TRAILER);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Header, body and trailer (FIX::Message::isEmpty, not FieldMap's body-only
|
|
203
|
+
// one), like clear().
|
|
204
|
+
Napi::Value MessageWrap::IsEmpty(const Napi::CallbackInfo& info) {
|
|
153
205
|
Napi::Env env = info.Env();
|
|
154
|
-
int tag = CoerceTag(env, info[0]);
|
|
155
206
|
NQ_TRY(env) {
|
|
156
|
-
|
|
157
|
-
return Napi::String::New(env, value);
|
|
207
|
+
return Napi::Boolean::New(env, message_.isEmpty());
|
|
158
208
|
} NQ_CATCH(env)
|
|
159
209
|
}
|
|
160
210
|
|
|
161
|
-
Napi::Value MessageWrap::
|
|
211
|
+
Napi::Value MessageWrap::TotalFields(const Napi::CallbackInfo& info) {
|
|
212
|
+
return fieldmap::TotalFields(info.Env(), message_);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Empties header, body and trailer.
|
|
216
|
+
Napi::Value MessageWrap::Clear(const Napi::CallbackInfo& info) {
|
|
162
217
|
Napi::Env env = info.Env();
|
|
163
|
-
int tag = CoerceTag(env, info[0]);
|
|
164
|
-
std::string value = CoerceValue(env, info[1]);
|
|
165
218
|
NQ_TRY(env) {
|
|
166
|
-
message_.
|
|
219
|
+
message_.clear();
|
|
167
220
|
} NQ_CATCH(env)
|
|
168
221
|
return env.Undefined();
|
|
169
222
|
}
|
|
170
223
|
|
|
224
|
+
Napi::Value MessageWrap::Fields(const Napi::CallbackInfo& info) {
|
|
225
|
+
return fieldmap::Fields(info.Env(), message_);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Napi::Value MessageWrap::HeaderFields(const Napi::CallbackInfo& info) {
|
|
229
|
+
return fieldmap::Fields(info.Env(), message_.getHeader());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Napi::Value MessageWrap::TrailerFields(const Napi::CallbackInfo& info) {
|
|
233
|
+
return fieldmap::Fields(info.Env(), message_.getTrailer());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Napi::Value MessageWrap::AddGroup(const Napi::CallbackInfo& info) {
|
|
237
|
+
return fieldmap::AddGroup(info, NQ_ROUTED);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Napi::Value MessageWrap::GetGroup(const Napi::CallbackInfo& info) {
|
|
241
|
+
return fieldmap::GetGroup(info, NQ_ROUTED);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Napi::Value MessageWrap::ReplaceGroup(const Napi::CallbackInfo& info) {
|
|
245
|
+
return fieldmap::ReplaceGroup(info, NQ_ROUTED);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Napi::Value MessageWrap::RemoveGroup(const Napi::CallbackInfo& info) {
|
|
249
|
+
return fieldmap::RemoveGroup(info, NQ_ROUTED);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Napi::Value MessageWrap::HasGroup(const Napi::CallbackInfo& info) {
|
|
253
|
+
return fieldmap::HasGroup(info, NQ_ROUTED);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Napi::Value MessageWrap::GroupCount(const Napi::CallbackInfo& info) {
|
|
257
|
+
return fieldmap::GroupCount(info, NQ_ROUTED);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#undef NQ_ROUTED
|
|
261
|
+
#undef NQ_HEADER
|
|
262
|
+
#undef NQ_TRAILER
|
|
263
|
+
|
|
171
264
|
Napi::Value MessageWrap::ToString(const Napi::CallbackInfo& info) {
|
|
172
265
|
Napi::Env env = info.Env();
|
|
173
266
|
NQ_TRY(env) {
|
package/cpp/message_wrap.h
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
#include <napi.h>
|
|
5
5
|
|
|
6
|
+
#include <memory>
|
|
7
|
+
|
|
8
|
+
#include "quickfix/DataDictionary.h"
|
|
6
9
|
#include "quickfix/Message.h"
|
|
7
10
|
|
|
8
11
|
namespace napi_quickfix {
|
|
@@ -10,17 +13,32 @@ namespace napi_quickfix {
|
|
|
10
13
|
// Napi::ObjectWrap over FIX::Message.
|
|
11
14
|
//
|
|
12
15
|
// new MessageWrap()
|
|
13
|
-
// new MessageWrap(raw: string, validate?: boolean = false
|
|
16
|
+
// new MessageWrap(raw: string, validate?: boolean = false,
|
|
17
|
+
// sessionDictionary?: DataDictionary,
|
|
18
|
+
// applicationDictionary?: DataDictionary)
|
|
19
|
+
//
|
|
20
|
+
// With a dictionary the raw string is parsed structurally (repeating groups
|
|
21
|
+
// become nested FieldMaps); without one it is parsed flat, as before.
|
|
14
22
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
23
|
+
// Field methods (getField / setField / isSetField / removeField /
|
|
24
|
+
// getFieldIfSet) and group methods (addGroup / getGroup / replaceGroup /
|
|
25
|
+
// removeGroup / hasGroup / groupCount) auto-route standard header/trailer
|
|
26
|
+
// tags to the right section, as well as the header/trailer fields declared by
|
|
27
|
+
// the session dictionary the message was parsed or delivered with. Any other
|
|
28
|
+
// tag goes to the section that already holds it (body first). isEmpty and
|
|
29
|
+
// clear() cover all three sections; totalFields / fields read the body.
|
|
17
30
|
class MessageWrap : public Napi::ObjectWrap<MessageWrap> {
|
|
18
31
|
public:
|
|
19
32
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
20
33
|
|
|
21
|
-
// Build a JS MessageWrap
|
|
22
|
-
//
|
|
34
|
+
// Build a JS MessageWrap holding a copy of `msg` (typed down to the last
|
|
35
|
+
// nested group entry, see fieldmap::CloneMessage), or taking `msg` over.
|
|
36
|
+
// The ApplicationBridge trampoline moves its own copy in, with the session
|
|
37
|
+
// dictionary that routes its fields (null: none).
|
|
23
38
|
static Napi::Object NewInstance(Napi::Env env, const FIX::Message& msg);
|
|
39
|
+
static Napi::Object NewInstance(
|
|
40
|
+
Napi::Env env, FIX::Message&& msg,
|
|
41
|
+
std::shared_ptr<const FIX::DataDictionary> sessionDictionary = nullptr);
|
|
24
42
|
|
|
25
43
|
explicit MessageWrap(const Napi::CallbackInfo& info);
|
|
26
44
|
|
|
@@ -35,17 +53,41 @@ class MessageWrap : public Napi::ObjectWrap<MessageWrap> {
|
|
|
35
53
|
private:
|
|
36
54
|
static Napi::FunctionReference constructor_;
|
|
37
55
|
|
|
56
|
+
// The section (header / body / trailer) that owns `tag`.
|
|
57
|
+
FIX::FieldMap& SectionFor(int tag);
|
|
58
|
+
|
|
38
59
|
Napi::Value GetField(const Napi::CallbackInfo& info);
|
|
39
60
|
Napi::Value SetField(const Napi::CallbackInfo& info);
|
|
61
|
+
Napi::Value IsSetField(const Napi::CallbackInfo& info);
|
|
62
|
+
Napi::Value RemoveField(const Napi::CallbackInfo& info);
|
|
63
|
+
Napi::Value GetFieldIfSet(const Napi::CallbackInfo& info);
|
|
40
64
|
Napi::Value GetHeaderField(const Napi::CallbackInfo& info);
|
|
41
65
|
Napi::Value SetHeaderField(const Napi::CallbackInfo& info);
|
|
42
66
|
Napi::Value GetTrailerField(const Napi::CallbackInfo& info);
|
|
43
67
|
Napi::Value SetTrailerField(const Napi::CallbackInfo& info);
|
|
68
|
+
Napi::Value IsEmpty(const Napi::CallbackInfo& info);
|
|
69
|
+
Napi::Value TotalFields(const Napi::CallbackInfo& info);
|
|
70
|
+
Napi::Value Clear(const Napi::CallbackInfo& info);
|
|
71
|
+
Napi::Value Fields(const Napi::CallbackInfo& info);
|
|
72
|
+
Napi::Value HeaderFields(const Napi::CallbackInfo& info);
|
|
73
|
+
Napi::Value TrailerFields(const Napi::CallbackInfo& info);
|
|
74
|
+
|
|
75
|
+
Napi::Value AddGroup(const Napi::CallbackInfo& info);
|
|
76
|
+
Napi::Value GetGroup(const Napi::CallbackInfo& info);
|
|
77
|
+
Napi::Value ReplaceGroup(const Napi::CallbackInfo& info);
|
|
78
|
+
Napi::Value RemoveGroup(const Napi::CallbackInfo& info);
|
|
79
|
+
Napi::Value HasGroup(const Napi::CallbackInfo& info);
|
|
80
|
+
Napi::Value GroupCount(const Napi::CallbackInfo& info);
|
|
81
|
+
|
|
44
82
|
Napi::Value ToString(const Napi::CallbackInfo& info);
|
|
45
83
|
Napi::Value ToPretty(const Napi::CallbackInfo& info);
|
|
46
84
|
Napi::Value GetMsgType(const Napi::CallbackInfo& info);
|
|
47
85
|
|
|
48
86
|
FIX::Message message_;
|
|
87
|
+
// The session dictionary whose header/trailer fields SectionFor routes
|
|
88
|
+
// (null: none). Shared, since the message may outlive the session or the
|
|
89
|
+
// JS DataDictionary it came from.
|
|
90
|
+
std::shared_ptr<const FIX::DataDictionary> sessionDictionary_;
|
|
49
91
|
};
|
|
50
92
|
|
|
51
93
|
} // namespace napi_quickfix
|
|
@@ -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
|