@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.
Files changed (145) hide show
  1. package/CMakeLists.txt +5 -1
  2. package/README.md +344 -18
  3. package/cpp/acceptor_wrap.cpp +253 -19
  4. package/cpp/acceptor_wrap.h +60 -1
  5. package/cpp/addon.cpp +10 -6
  6. package/cpp/application_bridge.cpp +114 -30
  7. package/cpp/application_bridge.h +57 -4
  8. package/cpp/data_dictionary_wrap.cpp +89 -3
  9. package/cpp/data_dictionary_wrap.h +27 -5
  10. package/cpp/engine_common.h +108 -0
  11. package/cpp/engine_workers.h +99 -18
  12. package/cpp/field_map_ops.h +492 -0
  13. package/cpp/group_wrap.cpp +221 -0
  14. package/cpp/group_wrap.h +76 -0
  15. package/cpp/initiator_wrap.cpp +262 -25
  16. package/cpp/initiator_wrap.h +37 -2
  17. package/cpp/message_wrap.cpp +171 -78
  18. package/cpp/message_wrap.h +47 -5
  19. package/cpp/session_op_gate.h +201 -0
  20. package/cpp/session_static.cpp +139 -12
  21. package/cpp/session_wrap.cpp +438 -0
  22. package/cpp/session_wrap.h +144 -0
  23. package/dist/cjs/acceptor.d.ts +0 -1
  24. package/dist/cjs/acceptor.js +0 -1
  25. package/dist/cjs/application.d.ts +0 -1
  26. package/dist/cjs/application.js +0 -1
  27. package/dist/cjs/data-dictionary.d.ts +21 -3
  28. package/dist/cjs/data-dictionary.js +32 -4
  29. package/dist/cjs/engine.d.ts +42 -4
  30. package/dist/cjs/engine.js +50 -5
  31. package/dist/cjs/enums.d.ts +48 -14
  32. package/dist/cjs/enums.js +20 -25
  33. package/dist/cjs/field-value.d.ts +12 -0
  34. package/dist/cjs/field-value.js +40 -0
  35. package/dist/cjs/generated/fields.d.ts +0 -1
  36. package/dist/cjs/generated/fields.js +0 -1
  37. package/dist/cjs/generated/values.d.ts +8548 -0
  38. package/dist/cjs/generated/values.js +8580 -0
  39. package/dist/cjs/group.d.ts +126 -0
  40. package/dist/cjs/group.js +185 -0
  41. package/dist/cjs/index.d.ts +28 -5
  42. package/dist/cjs/index.js +30 -28
  43. package/dist/cjs/initiator.d.ts +0 -1
  44. package/dist/cjs/initiator.js +0 -1
  45. package/dist/cjs/message.d.ts +129 -5
  46. package/dist/cjs/message.js +146 -9
  47. package/dist/cjs/native.d.ts +179 -17
  48. package/dist/cjs/native.js +0 -1
  49. package/dist/cjs/session-id.d.ts +12 -1
  50. package/dist/cjs/session-id.js +21 -1
  51. package/dist/cjs/session-settings.d.ts +0 -1
  52. package/dist/cjs/session-settings.js +0 -1
  53. package/dist/cjs/session.d.ts +213 -0
  54. package/dist/cjs/session.js +337 -0
  55. package/dist/cjs/tls-tunnel.d.ts +143 -0
  56. package/dist/cjs/tls-tunnel.js +261 -0
  57. package/dist/esm/acceptor.d.ts +0 -1
  58. package/dist/esm/acceptor.js +0 -1
  59. package/dist/esm/application.d.ts +0 -1
  60. package/dist/esm/application.js +0 -1
  61. package/dist/esm/data-dictionary.d.ts +21 -3
  62. package/dist/esm/data-dictionary.js +32 -4
  63. package/dist/esm/engine.d.ts +42 -4
  64. package/dist/esm/engine.js +51 -6
  65. package/dist/esm/enums.d.ts +48 -14
  66. package/dist/esm/enums.js +14 -24
  67. package/dist/esm/field-value.d.ts +12 -0
  68. package/dist/esm/field-value.js +37 -0
  69. package/dist/esm/generated/fields.d.ts +0 -1
  70. package/dist/esm/generated/fields.js +0 -1
  71. package/dist/esm/generated/values.d.ts +8548 -0
  72. package/dist/esm/generated/values.js +8564 -0
  73. package/dist/esm/group.d.ts +126 -0
  74. package/dist/esm/group.js +181 -0
  75. package/dist/esm/index.d.ts +28 -5
  76. package/dist/esm/index.js +18 -26
  77. package/dist/esm/initiator.d.ts +0 -1
  78. package/dist/esm/initiator.js +0 -1
  79. package/dist/esm/message.d.ts +129 -5
  80. package/dist/esm/message.js +146 -9
  81. package/dist/esm/native.d.ts +179 -17
  82. package/dist/esm/native.js +0 -1
  83. package/dist/esm/session-id.d.ts +12 -1
  84. package/dist/esm/session-id.js +20 -1
  85. package/dist/esm/session-settings.d.ts +0 -1
  86. package/dist/esm/session-settings.js +0 -1
  87. package/dist/esm/session.d.ts +213 -0
  88. package/dist/esm/session.js +329 -0
  89. package/dist/esm/tls-tunnel.d.ts +143 -0
  90. package/dist/esm/tls-tunnel.js +256 -0
  91. package/package.json +13 -1
  92. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  93. package/prebuilds/darwin-x64/node.napi.node +0 -0
  94. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  95. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  96. package/prebuilds/win32-x64/node.napi.node +0 -0
  97. package/cpp/enums.cpp +0 -66
  98. package/dist/cjs/acceptor.d.ts.map +0 -1
  99. package/dist/cjs/acceptor.js.map +0 -1
  100. package/dist/cjs/application.d.ts.map +0 -1
  101. package/dist/cjs/application.js.map +0 -1
  102. package/dist/cjs/data-dictionary.d.ts.map +0 -1
  103. package/dist/cjs/data-dictionary.js.map +0 -1
  104. package/dist/cjs/engine.d.ts.map +0 -1
  105. package/dist/cjs/engine.js.map +0 -1
  106. package/dist/cjs/enums.d.ts.map +0 -1
  107. package/dist/cjs/enums.js.map +0 -1
  108. package/dist/cjs/generated/fields.d.ts.map +0 -1
  109. package/dist/cjs/generated/fields.js.map +0 -1
  110. package/dist/cjs/index.d.ts.map +0 -1
  111. package/dist/cjs/index.js.map +0 -1
  112. package/dist/cjs/initiator.d.ts.map +0 -1
  113. package/dist/cjs/initiator.js.map +0 -1
  114. package/dist/cjs/message.d.ts.map +0 -1
  115. package/dist/cjs/message.js.map +0 -1
  116. package/dist/cjs/native.d.ts.map +0 -1
  117. package/dist/cjs/native.js.map +0 -1
  118. package/dist/cjs/session-id.d.ts.map +0 -1
  119. package/dist/cjs/session-id.js.map +0 -1
  120. package/dist/cjs/session-settings.d.ts.map +0 -1
  121. package/dist/cjs/session-settings.js.map +0 -1
  122. package/dist/esm/acceptor.d.ts.map +0 -1
  123. package/dist/esm/acceptor.js.map +0 -1
  124. package/dist/esm/application.d.ts.map +0 -1
  125. package/dist/esm/application.js.map +0 -1
  126. package/dist/esm/data-dictionary.d.ts.map +0 -1
  127. package/dist/esm/data-dictionary.js.map +0 -1
  128. package/dist/esm/engine.d.ts.map +0 -1
  129. package/dist/esm/engine.js.map +0 -1
  130. package/dist/esm/enums.d.ts.map +0 -1
  131. package/dist/esm/enums.js.map +0 -1
  132. package/dist/esm/generated/fields.d.ts.map +0 -1
  133. package/dist/esm/generated/fields.js.map +0 -1
  134. package/dist/esm/index.d.ts.map +0 -1
  135. package/dist/esm/index.js.map +0 -1
  136. package/dist/esm/initiator.d.ts.map +0 -1
  137. package/dist/esm/initiator.js.map +0 -1
  138. package/dist/esm/message.d.ts.map +0 -1
  139. package/dist/esm/message.js.map +0 -1
  140. package/dist/esm/native.d.ts.map +0 -1
  141. package/dist/esm/native.js.map +0 -1
  142. package/dist/esm/session-id.d.ts.map +0 -1
  143. package/dist/esm/session-id.js.map +0 -1
  144. package/dist/esm/session-settings.d.ts.map +0 -1
  145. package/dist/esm/session-settings.js.map +0 -1
@@ -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_ = msg;
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, "new Message(raw: string, validate?: boolean)");
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 && !info[1].IsUndefined() && !info[1].IsNull()) {
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
- message_.setString(raw, validate);
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
- Napi::Env env = info.Env();
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
- Napi::Env env = info.Env();
119
- int tag = CoerceTag(env, info[0]);
120
- std::string value = CoerceValue(env, info[1]);
121
- NQ_TRY(env) {
122
- if (FIX::Message::isHeaderField(tag)) {
123
- message_.getHeader().setField(tag, value);
124
- } else if (FIX::Message::isTrailerField(tag)) {
125
- message_.getTrailer().setField(tag, value);
126
- } else {
127
- message_.setField(tag, value);
128
- }
129
- } NQ_CATCH(env)
130
- return env.Undefined();
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
- Napi::Env env = info.Env();
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
- Napi::Env env = info.Env();
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
- const std::string& value = message_.getTrailer().getField(tag);
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::SetTrailerField(const Napi::CallbackInfo& info) {
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_.getTrailer().setField(tag, value);
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) {
@@ -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
- // Methods: getField / setField / getHeaderField / setHeaderField /
16
- // getTrailerField / setTrailerField / toString / toPretty / getMsgType.
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 wrapping a COPY of the given FIX::Message. Used by
22
- // the ApplicationBridge trampoline to hand messages to JS handlers.
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