@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
@@ -0,0 +1,492 @@
1
+ // FieldMap operations shared by MessageWrap and GroupWrap.
2
+ //
3
+ // FIX::Message (its body), FIX::Header, FIX::Trailer and FIX::Group are all
4
+ // FIX::FieldMap. The JS-facing field and repeating-group methods are therefore
5
+ // written once here as templates over a `Select` callable:
6
+ //
7
+ // FIX::FieldMap& select(int tag)
8
+ //
9
+ // which picks the FieldMap that owns `tag`. MessageWrap routes well-known
10
+ // header/trailer tags to the right section; GroupWrap always returns itself.
11
+ // Each op parses its own arguments from the CallbackInfo, so the wrappers are
12
+ // one-line delegations.
13
+ //
14
+ // Group indices are 1-based, as in QuickFIX.
15
+ #ifndef NAPI_QUICKFIX_FIELD_MAP_OPS_H
16
+ #define NAPI_QUICKFIX_FIELD_MAP_OPS_H
17
+
18
+ #include <napi.h>
19
+
20
+ #include <cstdint>
21
+ #include <limits>
22
+ #include <memory>
23
+ #include <string>
24
+ #include <utility>
25
+ #include <vector>
26
+
27
+ #include "errors.h"
28
+ #include "group_wrap.h"
29
+ #include "quickfix/FieldMap.h"
30
+ #include "quickfix/Group.h"
31
+ #include "quickfix/Message.h"
32
+
33
+ namespace napi_quickfix {
34
+ namespace fieldmap {
35
+
36
+ // --- argument coercion ------------------------------------------------------
37
+
38
+ inline bool IsNullish(Napi::Value v) { return v.IsUndefined() || v.IsNull(); }
39
+
40
+ // A JS number that is an integer within int range (no NaN, no fraction), as
41
+ // an int. Int32Value() would silently truncate 1.5 to 1 and NaN to 0.
42
+ inline bool ToInteger(Napi::Value v, int& out) {
43
+ if (!v.IsNumber()) {
44
+ return false;
45
+ }
46
+ const double d = v.As<Napi::Number>().DoubleValue();
47
+ if (!(d >= std::numeric_limits<int>::min() &&
48
+ d <= std::numeric_limits<int>::max())) {
49
+ return false; // NaN, infinities and out-of-range values all land here
50
+ }
51
+ const int i = static_cast<int>(d);
52
+ if (static_cast<double>(i) != d) {
53
+ return false;
54
+ }
55
+ out = i;
56
+ return true;
57
+ }
58
+
59
+ // A field tag for reads (getField, isSetField, ...): any integer. A tag that
60
+ // no message can hold (0, negative) simply reads as absent / FieldNotFound.
61
+ inline int CoerceTag(Napi::Env env, Napi::Value v) {
62
+ int tag;
63
+ if (!ToInteger(v, tag)) {
64
+ throw Napi::TypeError::New(env, "field tag must be an integer");
65
+ }
66
+ return tag;
67
+ }
68
+
69
+ // A field tag for writes (setField): a positive integer, since QuickFIX would
70
+ // happily emit "0=x" or "-1=x" on the wire.
71
+ inline int CoerceSetTag(Napi::Env env, Napi::Value v) {
72
+ int tag;
73
+ if (!ToInteger(v, tag) || tag <= 0) {
74
+ throw Napi::TypeError::New(env, "field tag must be a positive integer");
75
+ }
76
+ return tag;
77
+ }
78
+
79
+ // The largest tag accepted in a group's field order (delimiter included).
80
+ // QuickFIX's message_order allocates and zero-fills `largestTag + 1` ints per
81
+ // group, so an unbounded tag would mean a multi-GB allocation (or a signed
82
+ // overflow at INT_MAX) inside the Group constructor, where a std::bad_alloc
83
+ // is not translated and aborts the process. 2^20 keeps that at 4 MiB while
84
+ // leaving every standard and user-defined tag range (< 100000) usable.
85
+ constexpr int kMaxOrderTag = 1 << 20;
86
+
87
+ // A delimiter or group-order tag: a positive integer bounded by kMaxOrderTag.
88
+ inline int CheckOrderTag(Napi::Env env, int tag) {
89
+ if (tag > kMaxOrderTag) {
90
+ throw Napi::RangeError::New(
91
+ env, "group order field tags must be at most " +
92
+ std::to_string(kMaxOrderTag));
93
+ }
94
+ return tag;
95
+ }
96
+
97
+ // Group index (1-based): an integer. Range errors are handled per operation
98
+ // (FieldNotFound, false, or a no-op) since QuickFIX does not bound index 0.
99
+ inline int CoerceIndex(Napi::Env env, Napi::Value v) {
100
+ int index;
101
+ if (!ToInteger(v, index)) {
102
+ throw Napi::TypeError::New(env, "group index must be an integer");
103
+ }
104
+ return index;
105
+ }
106
+
107
+ // Coerce a JS arg to a FIX field value string. Accepts string or number
108
+ // (numbers are stringified without trailing ".0"). Throws TypeError otherwise.
109
+ inline std::string CoerceValue(Napi::Env env, Napi::Value v) {
110
+ if (v.IsString()) {
111
+ return v.As<Napi::String>().Utf8Value();
112
+ }
113
+ if (v.IsNumber()) {
114
+ double d = v.As<Napi::Number>().DoubleValue();
115
+ if (d == static_cast<double>(static_cast<long long>(d))) {
116
+ return std::to_string(static_cast<long long>(d));
117
+ }
118
+ return std::to_string(d);
119
+ }
120
+ throw Napi::TypeError::New(env, "field value must be a string or number");
121
+ }
122
+
123
+ // --- typed copies -----------------------------------------------------------
124
+
125
+ // The delimiter of a stored group entry. Entries are stored as FIX::Group
126
+ // (the ones QuickFIX parses with a dictionary, the ones AddGroup stores and
127
+ // the ones Clone rebuilds), so the dynamic type normally carries it. The
128
+ // fallback covers a plain-FieldMap entry QuickFIX itself stored (e.g. with
129
+ // FieldMap::addGroup, or by copying a message it queued out of order): its
130
+ // first field, which is the delimiter when the entry holds it (every group
131
+ // order sorts it first), and only a guess when it does not.
132
+ inline int DelimOf(const FIX::FieldMap& stored) {
133
+ if (const auto* g = dynamic_cast<const FIX::Group*>(&stored)) {
134
+ return g->delim();
135
+ }
136
+ if (stored.begin() != stored.end()) {
137
+ return stored.begin()->getTag();
138
+ }
139
+ return 0;
140
+ }
141
+
142
+ // The field order a FIX::Group is built with when its contents, field order
143
+ // included, are assigned over it right after (Clone, RebuildGroups' move).
144
+ // Group(field, delim) would build message_order(delim, 0), allocating and
145
+ // zero-filling delim + 1 ints only to discard them, and delim may be DelimOf's
146
+ // first-field guess, which kMaxOrderTag does not bound.
147
+ inline FIX::message_order PlaceholderOrder() {
148
+ return FIX::message_order(FIX::message_order::normal);
149
+ }
150
+
151
+ // FieldMap::operator= (hence every copy of a FIX::Message or FIX::Group,
152
+ // FIX::Group's copy constructor included) stores each nested entry as
153
+ // `new FieldMap(*entry)`: the fields and field order survive, but the entry's
154
+ // FIX::Group type, and with it its delimiter, is sliced away. Clone copies
155
+ // like operator= and then rebuilds every nested entry as a FIX::Group, so the
156
+ // delimiter of an entry that is empty or lacks its delimiter field is not lost
157
+ // on the way from the engine (or a JS Group) to a snapshot.
158
+
159
+ // Rebuild the nested entries of `dst`, a FieldMap::operator= copy of `src`, as
160
+ // FIX::Group. Walks the two group maps in lockstep (operator= preserves tags,
161
+ // counts and order) and swaps each sliced entry for a typed one that takes
162
+ // over its contents, so nothing is copied twice. The count tag is the map
163
+ // key: a QuickFIX-parsed entry's Group::field() is its first wire tag. The
164
+ // slot keeps its (hollowed) entry until the typed one is complete, so `dst`
165
+ // stays consistent should an allocation throw.
166
+ inline void RebuildGroups(FIX::FieldMap& dst, const FIX::FieldMap& src) {
167
+ FIX::FieldMap::g_iterator copied = dst.g_begin();
168
+ for (const auto& tagWithEntries : src.groups()) {
169
+ const int tag = tagWithEntries.first;
170
+ std::vector<FIX::FieldMap*>& slots = copied->second;
171
+ for (size_t i = 0; i < tagWithEntries.second.size(); i++) {
172
+ const FIX::FieldMap& original = *tagWithEntries.second[i];
173
+ auto typed = std::make_unique<FIX::Group>(tag, DelimOf(original),
174
+ PlaceholderOrder());
175
+ static_cast<FIX::FieldMap&>(*typed) = std::move(*slots[i]);
176
+ RebuildGroups(*typed, original);
177
+ delete slots[i];
178
+ slots[i] = typed.release();
179
+ }
180
+ ++copied;
181
+ }
182
+ }
183
+
184
+ // Assign `src` to `dst` (fields, nested groups and field order), keeping every
185
+ // nested entry a FIX::Group. `dst`'s own dynamic type is untouched: a Group
186
+ // keeps its count tag and delimiter.
187
+ inline void Clone(FIX::FieldMap& dst, const FIX::FieldMap& src) {
188
+ dst = src;
189
+ RebuildGroups(dst, src);
190
+ }
191
+
192
+ // Header, body and trailer of `src` into `dst`, nested entries typed.
193
+ inline void CloneMessage(FIX::Message& dst, const FIX::Message& src) {
194
+ dst = src;
195
+ RebuildGroups(dst.getHeader(), src.getHeader());
196
+ RebuildGroups(dst, src);
197
+ RebuildGroups(dst.getTrailer(), src.getTrailer());
198
+ }
199
+
200
+ // The same, by value. FIX::Message has a move constructor (unlike FIX::Group),
201
+ // so the returned copy stays typed whether or not the return is elided.
202
+ inline FIX::Message CloneMessage(const FIX::Message& src) {
203
+ FIX::Message copy;
204
+ CloneMessage(copy, src);
205
+ return copy;
206
+ }
207
+
208
+ // A heap FIX::Group copy of an entry of the `tag` repeating group, for
209
+ // FieldMap::addGroupPtr (which takes ownership).
210
+ inline FIX::Group* CloneEntry(int tag, const FIX::FieldMap& entry) {
211
+ auto group = std::make_unique<FIX::Group>(tag, DelimOf(entry),
212
+ PlaceholderOrder());
213
+ Clone(*group, entry);
214
+ return group.release();
215
+ }
216
+
217
+ // `[[tag, value], ...]` for the map's own fields (not its groups' fields).
218
+ inline Napi::Array Fields(Napi::Env env, const FIX::FieldMap& map) {
219
+ Napi::Array out = Napi::Array::New(env);
220
+ uint32_t i = 0;
221
+ for (const FIX::FieldBase& field : map) {
222
+ Napi::Array pair = Napi::Array::New(env, 2);
223
+ pair.Set(0u, Napi::Number::New(env, field.getTag()));
224
+ pair.Set(1u, Napi::String::New(env, field.getString()));
225
+ out.Set(i++, pair);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ // --- field ops (tag-routed) -------------------------------------------------
231
+
232
+ template <class Select>
233
+ Napi::Value GetField(const Napi::CallbackInfo& info, Select select) {
234
+ Napi::Env env = info.Env();
235
+ int tag = CoerceTag(env, info[0]);
236
+ NQ_TRY(env) {
237
+ return Napi::String::New(env, select(tag).getField(tag));
238
+ } NQ_CATCH(env)
239
+ }
240
+
241
+ template <class Select>
242
+ Napi::Value SetField(const Napi::CallbackInfo& info, Select select) {
243
+ Napi::Env env = info.Env();
244
+ int tag = CoerceSetTag(env, info[0]);
245
+ std::string value = CoerceValue(env, info[1]);
246
+ NQ_TRY(env) {
247
+ select(tag).setField(tag, value);
248
+ } NQ_CATCH(env)
249
+ return env.Undefined();
250
+ }
251
+
252
+ template <class Select>
253
+ Napi::Value IsSetField(const Napi::CallbackInfo& info, Select select) {
254
+ Napi::Env env = info.Env();
255
+ int tag = CoerceTag(env, info[0]);
256
+ NQ_TRY(env) {
257
+ return Napi::Boolean::New(env, select(tag).isSetField(tag));
258
+ } NQ_CATCH(env)
259
+ }
260
+
261
+ template <class Select>
262
+ Napi::Value RemoveField(const Napi::CallbackInfo& info, Select select) {
263
+ Napi::Env env = info.Env();
264
+ int tag = CoerceTag(env, info[0]);
265
+ NQ_TRY(env) {
266
+ select(tag).removeField(tag);
267
+ } NQ_CATCH(env)
268
+ return env.Undefined();
269
+ }
270
+
271
+ // string | undefined
272
+ template <class Select>
273
+ Napi::Value GetFieldIfSet(const Napi::CallbackInfo& info, Select select) {
274
+ Napi::Env env = info.Env();
275
+ int tag = CoerceTag(env, info[0]);
276
+ NQ_TRY(env) {
277
+ const FIX::FieldMap& map = select(tag);
278
+ if (!map.isSetField(tag)) {
279
+ return env.Undefined();
280
+ }
281
+ return Napi::String::New(env, map.getField(tag));
282
+ } NQ_CATCH(env)
283
+ }
284
+
285
+ // --- repeating-group ops (tag-routed) ---------------------------------------
286
+
287
+ // The delimiter of the first typed entry stored under `tag`, 0 when there is
288
+ // none: no entry, or only plain FieldMap entries, whose delimiter is unknown
289
+ // (see DelimOf).
290
+ inline int StoredDelim(const FIX::FieldMap& map, int tag) {
291
+ for (auto groups = map.g_begin(); groups != map.g_end(); ++groups) {
292
+ if (groups->first != tag) {
293
+ continue;
294
+ }
295
+ for (const FIX::FieldMap* entry : groups->second) {
296
+ if (const auto* typed = dynamic_cast<const FIX::Group*>(entry)) {
297
+ return typed->delim();
298
+ }
299
+ }
300
+ break;
301
+ }
302
+ return 0;
303
+ }
304
+
305
+ // addGroup(group: Group): appends a COPY of `group` under its own count tag and
306
+ // bumps the count field. The copy is typed down to the last nested entry (see
307
+ // Clone); the map takes ownership of the pointer.
308
+ // As in ReplaceGroup, an entry whose delimiter differs from the typed entries
309
+ // already stored under the tag is rejected: it would serialize as part of the
310
+ // previous entry, leaving the count field wrong.
311
+ template <class Select>
312
+ Napi::Value AddGroup(const Napi::CallbackInfo& info, Select select) {
313
+ Napi::Env env = info.Env();
314
+ GroupWrap* group = GroupWrap::UnwrapArg(env, info[0], "group");
315
+ const int delim = group->Group().delim();
316
+ int storedDelim = 0;
317
+ NQ_TRY(env) {
318
+ int tag = group->Group().field();
319
+ FIX::FieldMap& map = select(tag);
320
+ storedDelim = StoredDelim(map, tag);
321
+ if (storedDelim == 0 || storedDelim == delim) {
322
+ map.addGroupPtr(tag, CloneEntry(tag, group->Group()), true);
323
+ return env.Undefined();
324
+ }
325
+ } NQ_CATCH(env)
326
+ // Thrown outside NQ_TRY so it stays a TypeError.
327
+ throw Napi::TypeError::New(
328
+ env, "group delimiter " + std::to_string(delim) +
329
+ " does not match the stored entries' delimiter " +
330
+ std::to_string(storedDelim));
331
+ }
332
+
333
+ // getGroup(index, tag): Group — a snapshot copy of the index-th entry
334
+ // (1-based). Throws FieldNotFound when the tag has no groups or the index is
335
+ // out of range.
336
+ template <class Select>
337
+ Napi::Value GetGroup(const Napi::CallbackInfo& info, Select select) {
338
+ Napi::Env env = info.Env();
339
+ int index = CoerceIndex(env, info[0]);
340
+ int tag = CoerceTag(env, info[1]);
341
+ NQ_TRY(env) {
342
+ const FIX::FieldMap& stored = select(tag).getGroupRef(index, tag);
343
+ return GroupWrap::NewInstance(env, tag, DelimOf(stored), stored);
344
+ } NQ_CATCH(env)
345
+ }
346
+
347
+ // Whether the index-th entry (1-based) exists. QuickFIX's hasGroup(num, tag)
348
+ // is `groupCount(tag) >= num` with no lower bound, so it answers true for
349
+ // index 0 even on an empty tag; 0 is the typo a 1-based API must catch.
350
+ inline bool HasGroupAt(const FIX::FieldMap& map, int index, int tag) {
351
+ return index >= 1 && map.hasGroup(index, tag);
352
+ }
353
+
354
+ // The stored pointer of the index-th entry (1-based) of `tag`, which must
355
+ // exist (HasGroupAt).
356
+ inline FIX::FieldMap*& SlotOf(FIX::FieldMap& map, int index, int tag) {
357
+ FIX::FieldMap::g_iterator entries = map.g_begin();
358
+ while (entries->first != tag) {
359
+ ++entries;
360
+ }
361
+ return entries->second[index - 1];
362
+ }
363
+
364
+ // replaceGroup(index, group): swaps the index-th entry (1-based) under
365
+ // group.field() for a typed copy of `group` (FieldMap::replaceGroup would
366
+ // slice the nested entries). The copy is complete before the stored entry is
367
+ // touched, so `group` may contain that entry (`g.replaceGroup(1, g)`).
368
+ // QuickFIX silently ignores a missing slot (including index 0); we throw
369
+ // FieldNotFound instead so a typo cannot silently drop an update.
370
+ // A replacement whose delimiter differs from a typed stored entry's is
371
+ // rejected, as it would not belong to the same repeating group. A plain
372
+ // FieldMap entry (one QuickFIX copied, e.g. a message it queued out of order)
373
+ // has no known delimiter, DelimOf's first-field fallback being a guess, so
374
+ // any replacement is taken.
375
+ template <class Select>
376
+ Napi::Value ReplaceGroup(const Napi::CallbackInfo& info, Select select) {
377
+ Napi::Env env = info.Env();
378
+ int index = CoerceIndex(env, info[0]);
379
+ GroupWrap* group = GroupWrap::UnwrapArg(env, info[1], "group");
380
+ const int delim = group->Group().delim();
381
+ int storedDelim = 0;
382
+ NQ_TRY(env) {
383
+ int tag = group->Group().field();
384
+ FIX::FieldMap& map = select(tag);
385
+ if (!HasGroupAt(map, index, tag)) {
386
+ throw FIX::FieldNotFound(tag);
387
+ }
388
+ FIX::FieldMap*& slot = SlotOf(map, index, tag);
389
+ const auto* typed = dynamic_cast<const FIX::Group*>(slot);
390
+ storedDelim = typed != nullptr ? typed->delim() : delim;
391
+ if (storedDelim == delim) {
392
+ std::unique_ptr<FIX::FieldMap> copy(CloneEntry(tag, group->Group()));
393
+ delete slot;
394
+ slot = copy.release();
395
+ return env.Undefined();
396
+ }
397
+ } NQ_CATCH(env)
398
+ // Thrown outside NQ_TRY so it stays a TypeError.
399
+ throw Napi::TypeError::New(
400
+ env, "group delimiter " + std::to_string(delim) +
401
+ " does not match the stored entry's delimiter " +
402
+ std::to_string(storedDelim));
403
+ }
404
+
405
+ // The `(tag)` / `(index, tag)` argument shapes of removeGroup and hasGroup.
406
+ // Coercion happens outside NQ_TRY so a TypeError stays a TypeError (NQ_CATCH
407
+ // would translate it into a plain Error).
408
+ struct IndexAndTag {
409
+ bool hasIndex;
410
+ int index;
411
+ int tag;
412
+ };
413
+
414
+ inline IndexAndTag CoerceIndexAndTag(const Napi::CallbackInfo& info) {
415
+ Napi::Env env = info.Env();
416
+ if (info.Length() < 2 || IsNullish(info[1])) {
417
+ return {false, 0, CoerceTag(env, info[0])};
418
+ }
419
+ int index = CoerceIndex(env, info[0]);
420
+ return {true, index, CoerceTag(env, info[1])};
421
+ }
422
+
423
+ // removeGroup(tag) | removeGroup(index, tag). Removing every entry of a tag
424
+ // that has none is a no-op (as QuickFIX); removing a missing entry throws
425
+ // FieldNotFound, as replaceGroup does, where QuickFIX would silently ignore it
426
+ // (index 0, the 0-based typo, included).
427
+ template <class Select>
428
+ Napi::Value RemoveGroup(const Napi::CallbackInfo& info, Select select) {
429
+ Napi::Env env = info.Env();
430
+ const IndexAndTag args = CoerceIndexAndTag(info);
431
+ NQ_TRY(env) {
432
+ FIX::FieldMap& map = select(args.tag);
433
+ if (!args.hasIndex) {
434
+ map.removeGroup(args.tag);
435
+ } else if (HasGroupAt(map, args.index, args.tag)) {
436
+ map.removeGroup(args.index, args.tag);
437
+ } else {
438
+ throw FIX::FieldNotFound(args.tag);
439
+ }
440
+ } NQ_CATCH(env)
441
+ return env.Undefined();
442
+ }
443
+
444
+ // hasGroup(tag) | hasGroup(index, tag)
445
+ template <class Select>
446
+ Napi::Value HasGroup(const Napi::CallbackInfo& info, Select select) {
447
+ Napi::Env env = info.Env();
448
+ const IndexAndTag args = CoerceIndexAndTag(info);
449
+ NQ_TRY(env) {
450
+ const FIX::FieldMap& map = select(args.tag);
451
+ return Napi::Boolean::New(
452
+ env, args.hasIndex ? HasGroupAt(map, args.index, args.tag)
453
+ : map.hasGroup(args.tag));
454
+ } NQ_CATCH(env)
455
+ }
456
+
457
+ template <class Select>
458
+ Napi::Value GroupCount(const Napi::CallbackInfo& info, Select select) {
459
+ Napi::Env env = info.Env();
460
+ int tag = CoerceTag(env, info[0]);
461
+ NQ_TRY(env) {
462
+ return Napi::Number::New(
463
+ env, static_cast<double>(select(tag).groupCount(tag)));
464
+ } NQ_CATCH(env)
465
+ }
466
+
467
+ // --- whole-map ops ----------------------------------------------------------
468
+
469
+ inline Napi::Value IsEmpty(Napi::Env env, FIX::FieldMap& map) {
470
+ NQ_TRY(env) {
471
+ return Napi::Boolean::New(env, map.isEmpty());
472
+ } NQ_CATCH(env)
473
+ }
474
+
475
+ // Own fields plus every field of every (nested) group entry.
476
+ inline Napi::Value TotalFields(Napi::Env env, const FIX::FieldMap& map) {
477
+ NQ_TRY(env) {
478
+ return Napi::Number::New(env, static_cast<double>(map.totalFields()));
479
+ } NQ_CATCH(env)
480
+ }
481
+
482
+ inline Napi::Value Clear(Napi::Env env, FIX::FieldMap& map) {
483
+ NQ_TRY(env) {
484
+ map.clear();
485
+ } NQ_CATCH(env)
486
+ return env.Undefined();
487
+ }
488
+
489
+ } // namespace fieldmap
490
+ } // namespace napi_quickfix
491
+
492
+ #endif // NAPI_QUICKFIX_FIELD_MAP_OPS_H