@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
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#include "application_bridge.h"
|
|
2
2
|
|
|
3
|
+
#include <memory>
|
|
3
4
|
#include <mutex>
|
|
4
5
|
#include <string>
|
|
5
6
|
#include <utility>
|
|
6
7
|
|
|
8
|
+
#include "field_map_ops.h"
|
|
7
9
|
#include "message_wrap.h"
|
|
10
|
+
#include "quickfix/DataDictionaryProvider.h"
|
|
11
|
+
#include "quickfix/Session.h"
|
|
8
12
|
#include "session_id_wrap.h"
|
|
9
13
|
|
|
10
14
|
namespace napi_quickfix {
|
|
@@ -96,6 +100,29 @@ void ApplicationBridge::Abort() {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
bool ApplicationBridge::RunAfterQueued(Napi::Env env,
|
|
104
|
+
std::function<void(Napi::Env)> fn) {
|
|
105
|
+
if (released_ || inactive_.load(std::memory_order_acquire)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
auto* data = new CallData();
|
|
109
|
+
data->type = CallType::kBarrier;
|
|
110
|
+
data->hasMessage = false;
|
|
111
|
+
data->barrier = std::move(fn);
|
|
112
|
+
napi_status status = tsfn_.NonBlockingCall(data);
|
|
113
|
+
if (status != napi_ok) {
|
|
114
|
+
delete data;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// The loop must not exit before the barrier has run even if the user
|
|
118
|
+
// unref()'d the engine; after Release() the TSFN closes as soon as its queue
|
|
119
|
+
// is drained, so this only keeps the loop alive for that drain. Ref'd only
|
|
120
|
+
// once the item is queued: a refused call leaves the TSFN exactly as the
|
|
121
|
+
// caller had it (JS thread, so nothing runs in between).
|
|
122
|
+
tsfn_.Ref(env);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
void ApplicationBridge::Deactivate() {
|
|
100
127
|
inactive_.store(true, std::memory_order_release);
|
|
101
128
|
// Release any network thread already blocked in CallSync so the subsequent
|
|
@@ -104,12 +131,21 @@ void ApplicationBridge::Deactivate() {
|
|
|
104
131
|
WakeAllPending();
|
|
105
132
|
}
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
bool ApplicationBridge::RegisterChannel(const std::shared_ptr<SyncChannel>& ch,
|
|
135
|
+
std::uint64_t& id) {
|
|
109
136
|
std::lock_guard<std::mutex> lock(pendingMtx_);
|
|
110
|
-
|
|
137
|
+
// Checked under the lock: Deactivate()/Abort()/Release() store inactive_
|
|
138
|
+
// before they take pendingMtx_ for WakeAllPending(), so either this
|
|
139
|
+
// registration lands in pending_ before the wake loop iterates it, or it
|
|
140
|
+
// observes inactive_ here and is refused. Without this re-check a caller
|
|
141
|
+
// could pass CallSync's unlocked fast-path check, register after the wake
|
|
142
|
+
// loop ran, and wait until Abort() while the JS thread is blocked on it.
|
|
143
|
+
if (inactive_.load(std::memory_order_acquire)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
id = nextChannelId_++;
|
|
111
147
|
pending_[id] = ch;
|
|
112
|
-
return
|
|
148
|
+
return true;
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
void ApplicationBridge::UnregisterChannel(std::uint64_t id) {
|
|
@@ -164,37 +200,58 @@ void ApplicationBridge::FireAndForget(CallType type, const FIX::SessionID& id) {
|
|
|
164
200
|
}
|
|
165
201
|
}
|
|
166
202
|
|
|
167
|
-
ApplicationBridge::Result ApplicationBridge::CallSync(
|
|
168
|
-
|
|
169
|
-
const std::string& raw) {
|
|
203
|
+
ApplicationBridge::Result ApplicationBridge::CallSync(
|
|
204
|
+
CallType type, const FIX::SessionID& id, const FIX::Message& message) {
|
|
170
205
|
// Bridge is releasing/aborting: never call into the TSFN (its internals may be
|
|
171
206
|
// torn down during finalize -> calling would abort the process). Return an
|
|
172
207
|
// empty pass-through result so the QuickFIX thread proceeds without mutation
|
|
173
|
-
// or thrown exception.
|
|
208
|
+
// or thrown exception. This is only a cheap fast path; the authoritative
|
|
209
|
+
// check is the one RegisterChannel() makes under pendingMtx_.
|
|
174
210
|
if (inactive_.load(std::memory_order_acquire)) {
|
|
175
211
|
Result r;
|
|
176
212
|
return r;
|
|
177
213
|
}
|
|
178
214
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
// Everything that can throw (the copy, its serialization, the dictionary
|
|
216
|
+
// lookup) runs before the channel is registered, and `data` is owned until
|
|
217
|
+
// the TSFN takes it: an exception (std::bad_alloc) leaks nothing and leaves
|
|
218
|
+
// no channel in pending_.
|
|
219
|
+
auto data = std::make_unique<CallData>();
|
|
183
220
|
data->type = type;
|
|
184
221
|
data->sessionID = id;
|
|
185
|
-
|
|
222
|
+
// Deep copy, nested entries kept typed: the engine parsed them as
|
|
223
|
+
// FIX::Group with the session dictionary, and a plain copy would slice
|
|
224
|
+
// their delimiter away.
|
|
225
|
+
fieldmap::CloneMessage(data->message, message);
|
|
226
|
+
if (type == CallType::kToAdmin || type == CallType::kToApp) {
|
|
227
|
+
// An outbound message gets BodyLength (9) and CheckSum (10) only when
|
|
228
|
+
// Session::sendRaw serializes it, after toAdmin/toApp: serialize the
|
|
229
|
+
// clone so the handler sees them too. The engine recomputes both anyway.
|
|
230
|
+
data->message.toString();
|
|
231
|
+
}
|
|
232
|
+
data->sessionDictionary = SessionDictionary(id);
|
|
186
233
|
data->hasMessage = true;
|
|
234
|
+
|
|
235
|
+
auto channel = std::make_shared<SyncChannel>();
|
|
236
|
+
std::uint64_t channelId = 0;
|
|
237
|
+
if (!RegisterChannel(channel, channelId)) {
|
|
238
|
+
// Lost the race with Deactivate()/Abort(): the wake loop has already run
|
|
239
|
+
// (or is running) over pending_, so waiting now could block until Abort().
|
|
240
|
+
Result r;
|
|
241
|
+
return r;
|
|
242
|
+
}
|
|
187
243
|
data->channel = channel; // shared: trampoline gets its own owning copy
|
|
188
244
|
|
|
189
|
-
napi_status status = tsfn_.BlockingCall(data);
|
|
245
|
+
napi_status status = tsfn_.BlockingCall(data.get());
|
|
190
246
|
if (status != napi_ok) {
|
|
191
247
|
// TSFN is closing/aborted: it won't take ownership and won't fulfill the
|
|
192
|
-
// channel. Clean up and return a pass-through
|
|
193
|
-
|
|
248
|
+
// channel. Clean up (`data` frees itself) and return a pass-through
|
|
249
|
+
// result.
|
|
194
250
|
UnregisterChannel(channelId);
|
|
195
251
|
Result r;
|
|
196
252
|
return r;
|
|
197
253
|
}
|
|
254
|
+
data.release(); // the trampoline owns it now
|
|
198
255
|
|
|
199
256
|
// Wait for the trampoline (normal completion) or Abort/Deactivate (teardown)
|
|
200
257
|
// to signal the channel. Never blocks forever: WakeAllPending() sets done.
|
|
@@ -207,7 +264,30 @@ ApplicationBridge::Result ApplicationBridge::CallSync(CallType type,
|
|
|
207
264
|
return channel->result;
|
|
208
265
|
}
|
|
209
266
|
|
|
267
|
+
std::shared_ptr<const FIX::DataDictionary> ApplicationBridge::SessionDictionary(
|
|
268
|
+
const FIX::SessionID& id) {
|
|
269
|
+
std::lock_guard<std::mutex> lock(dictionariesMtx_);
|
|
270
|
+
auto found = dictionaries_.find(id);
|
|
271
|
+
if (found != dictionaries_.end()) {
|
|
272
|
+
return found->second;
|
|
273
|
+
}
|
|
274
|
+
FIX::Session* session = FIX::Session::lookupSession(id);
|
|
275
|
+
if (session == nullptr) {
|
|
276
|
+
return nullptr;
|
|
277
|
+
}
|
|
278
|
+
auto provider = std::make_shared<const FIX::DataDictionaryProvider>(
|
|
279
|
+
session->getDataDictionaryProvider());
|
|
280
|
+
std::shared_ptr<const FIX::DataDictionary> dictionary(
|
|
281
|
+
provider, &provider->getSessionDataDictionary(id.getBeginString()));
|
|
282
|
+
dictionaries_.emplace(id, dictionary);
|
|
283
|
+
return dictionary;
|
|
284
|
+
}
|
|
285
|
+
|
|
210
286
|
void ApplicationBridge::onCreate(const FIX::SessionID& id) {
|
|
287
|
+
{
|
|
288
|
+
std::lock_guard<std::mutex> lock(dictionariesMtx_);
|
|
289
|
+
dictionaries_.erase(id);
|
|
290
|
+
}
|
|
211
291
|
FireAndForget(CallType::kOnCreate, id);
|
|
212
292
|
}
|
|
213
293
|
|
|
@@ -221,22 +301,22 @@ void ApplicationBridge::onLogout(const FIX::SessionID& id) {
|
|
|
221
301
|
|
|
222
302
|
void ApplicationBridge::toAdmin(FIX::Message& message,
|
|
223
303
|
const FIX::SessionID& id) {
|
|
224
|
-
Result r = CallSync(CallType::kToAdmin, id, message
|
|
304
|
+
Result r = CallSync(CallType::kToAdmin, id, message);
|
|
225
305
|
if (r.mutated) {
|
|
226
|
-
message
|
|
306
|
+
message = r.editedMessage;
|
|
227
307
|
}
|
|
228
308
|
// toAdmin doesn't throw in the FIX interface; ignore r.threw here.
|
|
229
309
|
}
|
|
230
310
|
|
|
231
311
|
void ApplicationBridge::toApp(FIX::Message& message, const FIX::SessionID& id)
|
|
232
312
|
EXCEPT(FIX::DoNotSend) {
|
|
233
|
-
Result r = CallSync(CallType::kToApp, id, message
|
|
313
|
+
Result r = CallSync(CallType::kToApp, id, message);
|
|
234
314
|
if (r.threw) {
|
|
235
315
|
// toApp may throw DoNotSend.
|
|
236
316
|
throw FIX::DoNotSend();
|
|
237
317
|
}
|
|
238
318
|
if (r.mutated) {
|
|
239
|
-
message
|
|
319
|
+
message = r.editedMessage;
|
|
240
320
|
}
|
|
241
321
|
}
|
|
242
322
|
|
|
@@ -244,7 +324,7 @@ void ApplicationBridge::fromAdmin(const FIX::Message& message,
|
|
|
244
324
|
const FIX::SessionID& id)
|
|
245
325
|
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
246
326
|
FIX::RejectLogon) {
|
|
247
|
-
Result r = CallSync(CallType::kFromAdmin, id, message
|
|
327
|
+
Result r = CallSync(CallType::kFromAdmin, id, message);
|
|
248
328
|
if (r.threw) {
|
|
249
329
|
if (r.errorName == "RejectLogon") throw FIX::RejectLogon(r.errorMessage);
|
|
250
330
|
if (r.errorName == "IncorrectDataFormat") throw FIX::IncorrectDataFormat(0);
|
|
@@ -259,7 +339,7 @@ void ApplicationBridge::fromApp(const FIX::Message& message,
|
|
|
259
339
|
const FIX::SessionID& id)
|
|
260
340
|
EXCEPT(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue,
|
|
261
341
|
FIX::UnsupportedMessageType) {
|
|
262
|
-
Result r = CallSync(CallType::kFromApp, id, message
|
|
342
|
+
Result r = CallSync(CallType::kFromApp, id, message);
|
|
263
343
|
if (r.threw) {
|
|
264
344
|
if (r.errorName == "UnsupportedMessageType")
|
|
265
345
|
throw FIX::UnsupportedMessageType();
|
|
@@ -299,6 +379,12 @@ void ApplicationBridge::CallJs(Napi::Env env, Napi::Function /*jsCallback*/,
|
|
|
299
379
|
}
|
|
300
380
|
} guard{data, {}};
|
|
301
381
|
|
|
382
|
+
if (data->type == CallType::kBarrier) {
|
|
383
|
+
// Dropped (null env) or reached in FIFO order: either way, hand over.
|
|
384
|
+
if (data->barrier) data->barrier(env);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
302
388
|
// env may be null during environment teardown; nothing safe to do.
|
|
303
389
|
if (env == nullptr || context == nullptr) {
|
|
304
390
|
return;
|
|
@@ -324,13 +410,11 @@ void ApplicationBridge::CallJs(Napi::Env env, Napi::Function /*jsCallback*/,
|
|
|
324
410
|
Napi::Object msgObj;
|
|
325
411
|
bool haveMsg = false;
|
|
326
412
|
if (data->hasMessage) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
msgObj = MessageWrap::NewInstance(env, msg);
|
|
413
|
+
// Hand the structured copy over to the wrapper (data is deleted by the
|
|
414
|
+
// guard below): no string round-trip and no second copy, so the repeating
|
|
415
|
+
// groups the engine parsed stay intact for the handler.
|
|
416
|
+
msgObj = MessageWrap::NewInstance(env, std::move(data->message),
|
|
417
|
+
std::move(data->sessionDictionary));
|
|
334
418
|
haveMsg = true;
|
|
335
419
|
}
|
|
336
420
|
|
|
@@ -370,7 +454,7 @@ void ApplicationBridge::CallJs(Napi::Env env, Napi::Function /*jsCallback*/,
|
|
|
370
454
|
if (isSync && IsMutating(data->type) && haveMsg && !guard.result.threw) {
|
|
371
455
|
try {
|
|
372
456
|
MessageWrap* w = Napi::ObjectWrap<MessageWrap>::Unwrap(msgObj);
|
|
373
|
-
guard.result.editedMessage = w->Message()
|
|
457
|
+
guard.result.editedMessage = w->Message();
|
|
374
458
|
guard.result.mutated = true;
|
|
375
459
|
} catch (...) {
|
|
376
460
|
guard.result.mutated = false;
|
package/cpp/application_bridge.h
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
#include <atomic>
|
|
7
7
|
#include <condition_variable>
|
|
8
8
|
#include <cstdint>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <map>
|
|
9
11
|
#include <memory>
|
|
10
12
|
#include <mutex>
|
|
11
13
|
#include <string>
|
|
12
14
|
#include <unordered_map>
|
|
13
15
|
|
|
14
16
|
#include "quickfix/Application.h"
|
|
17
|
+
#include "quickfix/DataDictionary.h"
|
|
15
18
|
#include "quickfix/Message.h"
|
|
16
19
|
#include "quickfix/SessionID.h"
|
|
17
20
|
|
|
@@ -41,12 +44,19 @@ class ApplicationBridge : public FIX::Application {
|
|
|
41
44
|
kToApp,
|
|
42
45
|
kFromAdmin,
|
|
43
46
|
kFromApp,
|
|
47
|
+
// Not an Application callback: a closure queued by the JS thread through
|
|
48
|
+
// the same FIFO, so it runs after every event queued before it (see
|
|
49
|
+
// RunAfterQueued).
|
|
50
|
+
kBarrier,
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
// Result returned from the JS trampoline for synchronous calls.
|
|
47
54
|
struct Result {
|
|
48
55
|
// For mutating callbacks (toAdmin/toApp), the (possibly edited) message.
|
|
49
|
-
|
|
56
|
+
// Carried as a FIX::Message (not its wire string) so repeating groups the
|
|
57
|
+
// handler added survive: re-parsing a string without a dictionary would
|
|
58
|
+
// flatten them.
|
|
59
|
+
FIX::Message editedMessage;
|
|
50
60
|
bool mutated = false;
|
|
51
61
|
// If the JS handler threw, the FIX exception class name to re-throw
|
|
52
62
|
// (e.g. "DoNotSend", "RejectLogon", "UnsupportedMessageType") and message.
|
|
@@ -71,10 +81,21 @@ class ApplicationBridge : public FIX::Application {
|
|
|
71
81
|
struct CallData {
|
|
72
82
|
CallType type;
|
|
73
83
|
FIX::SessionID sessionID; // copied by value — safe across threads
|
|
74
|
-
|
|
84
|
+
// A deep copy of the message for message-bearing calls (a
|
|
85
|
+
// fieldmap::CloneMessage, which keeps every nested group entry typed).
|
|
86
|
+
// Copied, not serialized: the engine parsed it with the session's
|
|
87
|
+
// dictionary, and only the structured copy keeps its repeating groups for
|
|
88
|
+
// the JS handler, which takes the copy over.
|
|
89
|
+
FIX::Message message;
|
|
90
|
+
// The session dictionary the JS wrapper routes the message's fields by
|
|
91
|
+
// (see SessionDictionary); null when the session is gone.
|
|
92
|
+
std::shared_ptr<const FIX::DataDictionary> sessionDictionary;
|
|
75
93
|
bool hasMessage = false;
|
|
76
94
|
// For synchronous calls only: shared reply channel (null for fire-and-forget).
|
|
77
95
|
std::shared_ptr<SyncChannel> channel;
|
|
96
|
+
// kBarrier only. Invoked with a null Env if the item is dropped (TSFN
|
|
97
|
+
// aborted / environment teardown): then it must not touch JS.
|
|
98
|
+
std::function<void(Napi::Env)> barrier;
|
|
78
99
|
};
|
|
79
100
|
|
|
80
101
|
// The JS-thread trampoline. Runs on the Node event-loop thread. Declared
|
|
@@ -104,6 +125,15 @@ class ApplicationBridge : public FIX::Application {
|
|
|
104
125
|
void Ref(Napi::Env env);
|
|
105
126
|
void Unref(Napi::Env env);
|
|
106
127
|
|
|
128
|
+
// Queue `fn` to run on the JS thread AFTER every callback already queued
|
|
129
|
+
// (the TSFN queue is FIFO), and keep the event loop alive until it has run.
|
|
130
|
+
// JS thread only; call it before Release()/Abort(). Returns false, without
|
|
131
|
+
// queuing anything, if the bridge is no longer accepting work -- the caller
|
|
132
|
+
// then runs `fn` itself. `fn` receives a null Env if the queue is discarded
|
|
133
|
+
// before it is reached (Abort() / environment teardown) and must then do
|
|
134
|
+
// nothing that touches JS.
|
|
135
|
+
bool RunAfterQueued(Napi::Env env, std::function<void(Napi::Env)> fn);
|
|
136
|
+
|
|
107
137
|
// FIX::Application overrides.
|
|
108
138
|
void onCreate(const FIX::SessionID&) override;
|
|
109
139
|
void onLogon(const FIX::SessionID&) override;
|
|
@@ -122,7 +152,20 @@ class ApplicationBridge : public FIX::Application {
|
|
|
122
152
|
// Runs a synchronous call and returns the Result (throwing FIX exception is
|
|
123
153
|
// handled by the caller-side override, which inspects Result).
|
|
124
154
|
Result CallSync(CallType type, const FIX::SessionID& id,
|
|
125
|
-
const
|
|
155
|
+
const FIX::Message& message);
|
|
156
|
+
|
|
157
|
+
// The session dictionary of `id`, shared so a JS Message can keep it past
|
|
158
|
+
// the session's lifetime: a copy of the session's DataDictionaryProvider
|
|
159
|
+
// (which shares its dictionaries' ownership) aliased to the dictionary for
|
|
160
|
+
// the session's BeginString. Built once per session, on the QuickFIX thread
|
|
161
|
+
// running the callback (the session is alive there); onCreate drops the
|
|
162
|
+
// entry so a recreated session gets its own. Null if the session is not
|
|
163
|
+
// registered.
|
|
164
|
+
std::shared_ptr<const FIX::DataDictionary> SessionDictionary(
|
|
165
|
+
const FIX::SessionID& id);
|
|
166
|
+
std::mutex dictionariesMtx_;
|
|
167
|
+
std::map<FIX::SessionID, std::shared_ptr<const FIX::DataDictionary>>
|
|
168
|
+
dictionaries_;
|
|
126
169
|
|
|
127
170
|
Napi::ObjectReference handlers_;
|
|
128
171
|
TSFN tsfn_;
|
|
@@ -137,11 +180,21 @@ class ApplicationBridge : public FIX::Application {
|
|
|
137
180
|
// Wake all in-flight synchronous callers (used by Deactivate/Abort so a
|
|
138
181
|
// network thread blocked in CallSync doesn't wait forever when the main loop
|
|
139
182
|
// is frozen inside a cleanup hook). Registers each SyncChannel while pending.
|
|
183
|
+
//
|
|
184
|
+
// Invariant: every path that sets inactive_ stores it BEFORE taking
|
|
185
|
+
// pendingMtx_ to run WakeAllPending(), and RegisterChannel() re-checks
|
|
186
|
+
// inactive_ UNDER pendingMtx_. So a registration that saw inactive_ unset is
|
|
187
|
+
// already in pending_ when the wake loop runs, and one that lost the race is
|
|
188
|
+
// refused; either way no caller can start waiting after the wake. The JS
|
|
189
|
+
// thread relies on this when it blocks on a network thread after
|
|
190
|
+
// Deactivate() (Teardown's stop() join, DestroyEngine's gate freeze).
|
|
140
191
|
std::mutex pendingMtx_;
|
|
141
192
|
std::unordered_map<std::uint64_t, std::shared_ptr<SyncChannel>> pending_;
|
|
142
193
|
std::uint64_t nextChannelId_ = 0;
|
|
143
194
|
|
|
144
|
-
|
|
195
|
+
// Returns false (nothing registered) if the bridge is already inactive.
|
|
196
|
+
bool RegisterChannel(const std::shared_ptr<SyncChannel>& ch,
|
|
197
|
+
std::uint64_t& id);
|
|
145
198
|
void UnregisterChannel(std::uint64_t id);
|
|
146
199
|
void WakeAllPending();
|
|
147
200
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <string>
|
|
5
5
|
|
|
6
6
|
#include "errors.h"
|
|
7
|
+
#include "field_map_ops.h"
|
|
7
8
|
#include "message_wrap.h"
|
|
8
9
|
|
|
9
10
|
namespace napi_quickfix {
|
|
@@ -17,6 +18,11 @@ Napi::Object DataDictionaryWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
|
17
18
|
StaticMethod("fromFile", &DataDictionaryWrap::FromFile),
|
|
18
19
|
StaticMethod("fromString", &DataDictionaryWrap::FromString),
|
|
19
20
|
InstanceMethod("validate", &DataDictionaryWrap::Validate),
|
|
21
|
+
InstanceMethod("getVersion", &DataDictionaryWrap::GetVersion),
|
|
22
|
+
InstanceMethod("getFieldName", &DataDictionaryWrap::GetFieldName),
|
|
23
|
+
InstanceMethod("getFieldTag", &DataDictionaryWrap::GetFieldTag),
|
|
24
|
+
InstanceMethod("isField", &DataDictionaryWrap::IsField),
|
|
25
|
+
InstanceMethod("isMsgType", &DataDictionaryWrap::IsMsgType),
|
|
20
26
|
});
|
|
21
27
|
|
|
22
28
|
constructor_ = Napi::Persistent(func);
|
|
@@ -26,6 +32,17 @@ Napi::Object DataDictionaryWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
|
26
32
|
return exports;
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
DataDictionaryWrap* DataDictionaryWrap::UnwrapArg(Napi::Env env,
|
|
36
|
+
Napi::Value value,
|
|
37
|
+
const char* argName) {
|
|
38
|
+
if (!value.IsObject() ||
|
|
39
|
+
!value.As<Napi::Object>().InstanceOf(constructor_.Value())) {
|
|
40
|
+
throw Napi::TypeError::New(
|
|
41
|
+
env, std::string(argName) + " must be a DataDictionary instance");
|
|
42
|
+
}
|
|
43
|
+
return Napi::ObjectWrap<DataDictionaryWrap>::Unwrap(value.As<Napi::Object>());
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
// Constructor is internal: (kind: "string"|"file", payload: string).
|
|
30
47
|
DataDictionaryWrap::DataDictionaryWrap(const Napi::CallbackInfo& info)
|
|
31
48
|
: Napi::ObjectWrap<DataDictionaryWrap>(info) {
|
|
@@ -42,9 +59,9 @@ DataDictionaryWrap::DataDictionaryWrap(const Napi::CallbackInfo& info)
|
|
|
42
59
|
NQ_TRY(env) {
|
|
43
60
|
if (kind == "string") {
|
|
44
61
|
std::istringstream stream(payload);
|
|
45
|
-
dict_
|
|
62
|
+
dict_->readFromStream(stream);
|
|
46
63
|
} else { // "file"
|
|
47
|
-
dict_
|
|
64
|
+
dict_->readFromURL(payload);
|
|
48
65
|
}
|
|
49
66
|
} NQ_CATCH(env)
|
|
50
67
|
}
|
|
@@ -67,13 +84,82 @@ Napi::Value DataDictionaryWrap::FromString(const Napi::CallbackInfo& info) {
|
|
|
67
84
|
{Napi::String::New(env, "string"), info[0].As<Napi::String>()});
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
// validate(message, bodyOnly?: boolean). With bodyOnly the dictionary is used
|
|
88
|
+
// as the application dictionary only (QuickFIX's `validate(msg, true)`): no
|
|
89
|
+
// BeginString version check and no tag/value validation of the header and
|
|
90
|
+
// trailer fields. The header still needs BeginString and MsgType, and every
|
|
91
|
+
// field the dictionary requires, header and trailer included, must be present.
|
|
70
92
|
Napi::Value DataDictionaryWrap::Validate(const Napi::CallbackInfo& info) {
|
|
71
93
|
Napi::Env env = info.Env();
|
|
72
94
|
MessageWrap* msg = MessageWrap::UnwrapArg(env, info[0], "message");
|
|
95
|
+
bool bodyOnly = false;
|
|
96
|
+
if (info.Length() >= 2 && !fieldmap::IsNullish(info[1])) {
|
|
97
|
+
bodyOnly = info[1].ToBoolean().Value();
|
|
98
|
+
}
|
|
73
99
|
NQ_TRY(env) {
|
|
74
|
-
dict_
|
|
100
|
+
dict_->validate(msg->Message(), bodyOnly);
|
|
75
101
|
} NQ_CATCH(env)
|
|
76
102
|
return env.Undefined();
|
|
77
103
|
}
|
|
78
104
|
|
|
105
|
+
// The BeginString the spec declares (e.g. "FIX.4.4"), "" if it has none.
|
|
106
|
+
Napi::Value DataDictionaryWrap::GetVersion(const Napi::CallbackInfo& info) {
|
|
107
|
+
Napi::Env env = info.Env();
|
|
108
|
+
NQ_TRY(env) {
|
|
109
|
+
return Napi::String::New(env, dict_->getVersion());
|
|
110
|
+
} NQ_CATCH(env)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Napi::Value DataDictionaryWrap::GetFieldName(const Napi::CallbackInfo& info) {
|
|
114
|
+
Napi::Env env = info.Env();
|
|
115
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
116
|
+
throw Napi::TypeError::New(env, "getFieldName(tag: number)");
|
|
117
|
+
}
|
|
118
|
+
int tag = fieldmap::CoerceTag(env, info[0]);
|
|
119
|
+
NQ_TRY(env) {
|
|
120
|
+
std::string name;
|
|
121
|
+
if (!dict_->getFieldName(tag, name)) {
|
|
122
|
+
return env.Undefined();
|
|
123
|
+
}
|
|
124
|
+
return Napi::String::New(env, name);
|
|
125
|
+
} NQ_CATCH(env)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Napi::Value DataDictionaryWrap::GetFieldTag(const Napi::CallbackInfo& info) {
|
|
129
|
+
Napi::Env env = info.Env();
|
|
130
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
131
|
+
throw Napi::TypeError::New(env, "getFieldTag(name: string)");
|
|
132
|
+
}
|
|
133
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
134
|
+
NQ_TRY(env) {
|
|
135
|
+
int tag = 0;
|
|
136
|
+
if (!dict_->getFieldTag(name, tag)) {
|
|
137
|
+
return env.Undefined();
|
|
138
|
+
}
|
|
139
|
+
return Napi::Number::New(env, tag);
|
|
140
|
+
} NQ_CATCH(env)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Napi::Value DataDictionaryWrap::IsField(const Napi::CallbackInfo& info) {
|
|
144
|
+
Napi::Env env = info.Env();
|
|
145
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
146
|
+
throw Napi::TypeError::New(env, "isField(tag: number)");
|
|
147
|
+
}
|
|
148
|
+
int tag = fieldmap::CoerceTag(env, info[0]);
|
|
149
|
+
NQ_TRY(env) {
|
|
150
|
+
return Napi::Boolean::New(env, dict_->isField(tag));
|
|
151
|
+
} NQ_CATCH(env)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Napi::Value DataDictionaryWrap::IsMsgType(const Napi::CallbackInfo& info) {
|
|
155
|
+
Napi::Env env = info.Env();
|
|
156
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
157
|
+
throw Napi::TypeError::New(env, "isMsgType(msgType: string)");
|
|
158
|
+
}
|
|
159
|
+
std::string msgType = info[0].As<Napi::String>().Utf8Value();
|
|
160
|
+
NQ_TRY(env) {
|
|
161
|
+
return Napi::Boolean::New(env, dict_->isMsgType(msgType));
|
|
162
|
+
} NQ_CATCH(env)
|
|
163
|
+
}
|
|
164
|
+
|
|
79
165
|
} // namespace napi_quickfix
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
#include <napi.h>
|
|
5
5
|
|
|
6
|
+
#include <memory>
|
|
7
|
+
|
|
6
8
|
#include "quickfix/DataDictionary.h"
|
|
7
9
|
|
|
8
10
|
namespace napi_quickfix {
|
|
@@ -11,15 +13,29 @@ namespace napi_quickfix {
|
|
|
11
13
|
//
|
|
12
14
|
// DataDictionary.fromFile(path): DataDictionary
|
|
13
15
|
// DataDictionary.fromString(xml): DataDictionary
|
|
14
|
-
// dd.validate(message: Message): void
|
|
16
|
+
// dd.validate(message: Message, bodyOnly?: boolean): void
|
|
17
|
+
// (throws QuickFixError if invalid)
|
|
18
|
+
// dd.getVersion(): string
|
|
19
|
+
// dd.getFieldName(tag): string | undefined
|
|
20
|
+
// dd.getFieldTag(name): number | undefined
|
|
21
|
+
// dd.isField(tag): boolean
|
|
22
|
+
// dd.isMsgType(msgType): boolean
|
|
15
23
|
class DataDictionaryWrap : public Napi::ObjectWrap<DataDictionaryWrap> {
|
|
16
24
|
public:
|
|
17
25
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
18
26
|
|
|
19
27
|
explicit DataDictionaryWrap(const Napi::CallbackInfo& info);
|
|
20
28
|
|
|
21
|
-
FIX::DataDictionary& Dictionary() { return dict_; }
|
|
22
|
-
const FIX::DataDictionary& Dictionary() const { return dict_; }
|
|
29
|
+
FIX::DataDictionary& Dictionary() { return *dict_; }
|
|
30
|
+
const FIX::DataDictionary& Dictionary() const { return *dict_; }
|
|
31
|
+
// Shared ownership, for a Message that routes its fields by this dictionary
|
|
32
|
+
// and may outlive the JS DataDictionary.
|
|
33
|
+
std::shared_ptr<const FIX::DataDictionary> Shared() const { return dict_; }
|
|
34
|
+
|
|
35
|
+
// Unwrap a JS value that must be a DataDictionaryWrap; throws
|
|
36
|
+
// Napi::TypeError if not.
|
|
37
|
+
static DataDictionaryWrap* UnwrapArg(Napi::Env env, Napi::Value value,
|
|
38
|
+
const char* argName);
|
|
23
39
|
|
|
24
40
|
private:
|
|
25
41
|
static Napi::FunctionReference constructor_;
|
|
@@ -28,8 +44,14 @@ class DataDictionaryWrap : public Napi::ObjectWrap<DataDictionaryWrap> {
|
|
|
28
44
|
static Napi::Value FromString(const Napi::CallbackInfo& info);
|
|
29
45
|
|
|
30
46
|
Napi::Value Validate(const Napi::CallbackInfo& info);
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
Napi::Value GetVersion(const Napi::CallbackInfo& info);
|
|
48
|
+
Napi::Value GetFieldName(const Napi::CallbackInfo& info);
|
|
49
|
+
Napi::Value GetFieldTag(const Napi::CallbackInfo& info);
|
|
50
|
+
Napi::Value IsField(const Napi::CallbackInfo& info);
|
|
51
|
+
Napi::Value IsMsgType(const Napi::CallbackInfo& info);
|
|
52
|
+
|
|
53
|
+
std::shared_ptr<FIX::DataDictionary> dict_ =
|
|
54
|
+
std::make_shared<FIX::DataDictionary>();
|
|
33
55
|
};
|
|
34
56
|
|
|
35
57
|
} // namespace napi_quickfix
|