@homaiohq/napi-quickfix 0.0.0-stage → 0.1.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 +131 -0
- package/LICENSE +21 -0
- package/README.md +421 -2
- package/THIRD_PARTY_LICENSES +48 -0
- package/cpp/acceptor_wrap.cpp +234 -0
- package/cpp/acceptor_wrap.h +57 -0
- package/cpp/addon.cpp +83 -0
- package/cpp/application_bridge.cpp +382 -0
- package/cpp/application_bridge.h +151 -0
- package/cpp/data_dictionary_wrap.cpp +79 -0
- package/cpp/data_dictionary_wrap.h +37 -0
- package/cpp/engine_common.cpp +5 -0
- package/cpp/engine_common.h +19 -0
- package/cpp/engine_workers.h +86 -0
- package/cpp/enums.cpp +66 -0
- package/cpp/errors.h +142 -0
- package/cpp/initiator_wrap.cpp +255 -0
- package/cpp/initiator_wrap.h +69 -0
- package/cpp/message_wrap.cpp +195 -0
- package/cpp/message_wrap.h +53 -0
- package/cpp/session_id_wrap.cpp +91 -0
- package/cpp/session_id_wrap.h +46 -0
- package/cpp/session_settings_wrap.cpp +98 -0
- package/cpp/session_settings_wrap.h +40 -0
- package/cpp/session_static.cpp +93 -0
- package/dist/cjs/acceptor.d.ts +30 -0
- package/dist/cjs/acceptor.d.ts.map +1 -0
- package/dist/cjs/acceptor.js +39 -0
- package/dist/cjs/acceptor.js.map +1 -0
- package/dist/cjs/application.d.ts +116 -0
- package/dist/cjs/application.d.ts.map +1 -0
- package/dist/cjs/application.js +63 -0
- package/dist/cjs/application.js.map +1 -0
- package/dist/cjs/data-dictionary.d.ts +49 -0
- package/dist/cjs/data-dictionary.d.ts.map +1 -0
- package/dist/cjs/data-dictionary.js +77 -0
- package/dist/cjs/data-dictionary.js.map +1 -0
- package/dist/cjs/engine.d.ts +114 -0
- package/dist/cjs/engine.d.ts.map +1 -0
- package/dist/cjs/engine.js +138 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/enums.d.ts +22 -0
- package/dist/cjs/enums.d.ts.map +1 -0
- package/dist/cjs/enums.js +49 -0
- package/dist/cjs/enums.js.map +1 -0
- package/dist/cjs/generated/fields.d.ts +6123 -0
- package/dist/cjs/generated/fields.d.ts.map +1 -0
- package/dist/cjs/generated/fields.js +6128 -0
- package/dist/cjs/generated/fields.js.map +1 -0
- package/dist/cjs/index.d.ts +47 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/initiator.d.ts +30 -0
- package/dist/cjs/initiator.d.ts.map +1 -0
- package/dist/cjs/initiator.js +39 -0
- package/dist/cjs/initiator.js.map +1 -0
- package/dist/cjs/load-native.cjs +5 -0
- package/dist/cjs/message.d.ts +114 -0
- package/dist/cjs/message.d.ts.map +1 -0
- package/dist/cjs/message.js +162 -0
- package/dist/cjs/message.js.map +1 -0
- package/dist/cjs/native.d.ts +163 -0
- package/dist/cjs/native.d.ts.map +1 -0
- package/dist/cjs/native.js +23 -0
- package/dist/cjs/native.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/session-id.d.ts +40 -0
- package/dist/cjs/session-id.d.ts.map +1 -0
- package/dist/cjs/session-id.js +67 -0
- package/dist/cjs/session-id.js.map +1 -0
- package/dist/cjs/session-settings.d.ts +47 -0
- package/dist/cjs/session-settings.d.ts.map +1 -0
- package/dist/cjs/session-settings.js +76 -0
- package/dist/cjs/session-settings.js.map +1 -0
- package/dist/esm/acceptor.d.ts +30 -0
- package/dist/esm/acceptor.d.ts.map +1 -0
- package/dist/esm/acceptor.js +35 -0
- package/dist/esm/acceptor.js.map +1 -0
- package/dist/esm/application.d.ts +116 -0
- package/dist/esm/application.d.ts.map +1 -0
- package/dist/esm/application.js +58 -0
- package/dist/esm/application.js.map +1 -0
- package/dist/esm/data-dictionary.d.ts +49 -0
- package/dist/esm/data-dictionary.d.ts.map +1 -0
- package/dist/esm/data-dictionary.js +73 -0
- package/dist/esm/data-dictionary.js.map +1 -0
- package/dist/esm/engine.d.ts +114 -0
- package/dist/esm/engine.d.ts.map +1 -0
- package/dist/esm/engine.js +134 -0
- package/dist/esm/engine.js.map +1 -0
- package/dist/esm/enums.d.ts +22 -0
- package/dist/esm/enums.d.ts.map +1 -0
- package/dist/esm/enums.js +45 -0
- package/dist/esm/enums.js.map +1 -0
- package/dist/esm/generated/fields.d.ts +6123 -0
- package/dist/esm/generated/fields.d.ts.map +1 -0
- package/dist/esm/generated/fields.js +6125 -0
- package/dist/esm/generated/fields.js.map +1 -0
- package/dist/esm/index.d.ts +47 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +52 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/initiator.d.ts +30 -0
- package/dist/esm/initiator.d.ts.map +1 -0
- package/dist/esm/initiator.js +35 -0
- package/dist/esm/initiator.js.map +1 -0
- package/dist/esm/load-native.cjs +5 -0
- package/dist/esm/message.d.ts +114 -0
- package/dist/esm/message.d.ts.map +1 -0
- package/dist/esm/message.js +156 -0
- package/dist/esm/message.js.map +1 -0
- package/dist/esm/native.d.ts +163 -0
- package/dist/esm/native.d.ts.map +1 -0
- package/dist/esm/native.js +17 -0
- package/dist/esm/native.js.map +1 -0
- package/dist/esm/session-id.d.ts +40 -0
- package/dist/esm/session-id.d.ts.map +1 -0
- package/dist/esm/session-id.js +63 -0
- package/dist/esm/session-id.js.map +1 -0
- package/dist/esm/session-settings.d.ts +47 -0
- package/dist/esm/session-settings.d.ts.map +1 -0
- package/dist/esm/session-settings.js +72 -0
- package/dist/esm/session-settings.js.map +1 -0
- package/package.json +83 -4
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
- package/prebuilds/linux-x64/node.napi.musl.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/src/load-native.cjs +5 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_ENGINE_WORKERS_H
|
|
2
|
+
#define NAPI_QUICKFIX_ENGINE_WORKERS_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <utility>
|
|
8
|
+
|
|
9
|
+
#include "errors.h"
|
|
10
|
+
|
|
11
|
+
namespace napi_quickfix {
|
|
12
|
+
|
|
13
|
+
// A generic AsyncWorker that runs a blocking QuickFIX engine operation
|
|
14
|
+
// (start/stop) on a libuv worker thread and resolves/rejects a JS Promise.
|
|
15
|
+
//
|
|
16
|
+
// Why off-thread: start()/stop() synchronously invoke FIX::Application callbacks
|
|
17
|
+
// (toAdmin/toApp during send-on-logon, onLogout during stop) which the
|
|
18
|
+
// ApplicationBridge dispatches back to the JS thread via a TSFN BlockingCall +
|
|
19
|
+
// future. If the operation ran ON the JS thread, the event loop couldn't service
|
|
20
|
+
// the TSFN trampoline and we'd deadlock. Running it on a worker thread keeps the
|
|
21
|
+
// main event loop free to run CallJs.
|
|
22
|
+
//
|
|
23
|
+
// Lifetime: the worker holds a Napi::ObjectReference to the owning wrap object so
|
|
24
|
+
// it (and its bridge / engine / factories) can't be GC'd while the op is in
|
|
25
|
+
// flight. The ref is released in OnOK/OnError (both run on the JS thread).
|
|
26
|
+
class EngineOpWorker : public Napi::AsyncWorker {
|
|
27
|
+
public:
|
|
28
|
+
// onSettled receives `true` if the op succeeded, `false` if it threw.
|
|
29
|
+
EngineOpWorker(Napi::Env env, Napi::Object owner, std::function<void()> op,
|
|
30
|
+
std::function<void(bool)> onSettled)
|
|
31
|
+
: Napi::AsyncWorker(env),
|
|
32
|
+
deferred_(Napi::Promise::Deferred::New(env)),
|
|
33
|
+
op_(std::move(op)),
|
|
34
|
+
onSettled_(std::move(onSettled)) {
|
|
35
|
+
ownerRef_ = Napi::Persistent(owner);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Napi::Promise Promise() { return deferred_.Promise(); }
|
|
39
|
+
|
|
40
|
+
// Runs on a libuv worker thread. NO Napi/JS access allowed here.
|
|
41
|
+
void Execute() override {
|
|
42
|
+
try {
|
|
43
|
+
op_();
|
|
44
|
+
} catch (const std::exception& e) {
|
|
45
|
+
err_.Capture(e);
|
|
46
|
+
} catch (...) {
|
|
47
|
+
err_.has = true;
|
|
48
|
+
err_.message = "unknown native error";
|
|
49
|
+
err_.fixErrorName = "Error";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Runs on the JS thread.
|
|
54
|
+
void OnOK() override {
|
|
55
|
+
Napi::Env env = Env();
|
|
56
|
+
Napi::HandleScope scope(env);
|
|
57
|
+
if (onSettled_) onSettled_(!err_.has);
|
|
58
|
+
if (err_.has) {
|
|
59
|
+
deferred_.Reject(err_.ToError(env).Value());
|
|
60
|
+
} else {
|
|
61
|
+
deferred_.Resolve(env.Undefined());
|
|
62
|
+
}
|
|
63
|
+
ownerRef_.Reset();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Runs on the JS thread (only if AsyncWorker::SetError was used; we don't, so
|
|
67
|
+
// errors flow through OnOK — but provide OnError for completeness/safety).
|
|
68
|
+
void OnError(const Napi::Error& e) override {
|
|
69
|
+
Napi::Env env = Env();
|
|
70
|
+
Napi::HandleScope scope(env);
|
|
71
|
+
if (onSettled_) onSettled_(false);
|
|
72
|
+
deferred_.Reject(e.Value());
|
|
73
|
+
ownerRef_.Reset();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private:
|
|
77
|
+
Napi::Promise::Deferred deferred_;
|
|
78
|
+
Napi::ObjectReference ownerRef_;
|
|
79
|
+
std::function<void()> op_;
|
|
80
|
+
std::function<void(bool)> onSettled_;
|
|
81
|
+
CapturedError err_;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
} // namespace napi_quickfix
|
|
85
|
+
|
|
86
|
+
#endif // NAPI_QUICKFIX_ENGINE_WORKERS_H
|
package/cpp/enums.cpp
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
|
|
3
|
+
namespace napi_quickfix {
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
void SetInt(Napi::Object obj, const char* key, int value) {
|
|
8
|
+
obj.Set(key, Napi::Number::New(obj.Env(), value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
void SetStr(Napi::Object obj, const char* key, const char* value) {
|
|
12
|
+
obj.Set(key, Napi::String::New(obj.Env(), value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
} // namespace
|
|
16
|
+
|
|
17
|
+
// Exposes commonly-needed FIX enum values (MsgType, Side, ...) as `exports.enums`.
|
|
18
|
+
// Field tags are NOT here: `FIELD` is generated on the TS side from QuickFIX's
|
|
19
|
+
// FixFieldNumbers.h (scripts/gen-fields.mjs) so it covers every field.
|
|
20
|
+
// Not exhaustive by design — a curated, useful subset.
|
|
21
|
+
void RegisterEnums(Napi::Env env, Napi::Object exports) {
|
|
22
|
+
Napi::Object enums = Napi::Object::New(env);
|
|
23
|
+
|
|
24
|
+
// --- MsgType ------------------------------------------------------------
|
|
25
|
+
Napi::Object msgType = Napi::Object::New(env);
|
|
26
|
+
SetStr(msgType, "Heartbeat", "0");
|
|
27
|
+
SetStr(msgType, "TestRequest", "1");
|
|
28
|
+
SetStr(msgType, "ResendRequest", "2");
|
|
29
|
+
SetStr(msgType, "Reject", "3");
|
|
30
|
+
SetStr(msgType, "SequenceReset", "4");
|
|
31
|
+
SetStr(msgType, "Logout", "5");
|
|
32
|
+
SetStr(msgType, "Logon", "A");
|
|
33
|
+
SetStr(msgType, "NewOrderSingle", "D");
|
|
34
|
+
SetStr(msgType, "ExecutionReport", "8");
|
|
35
|
+
SetStr(msgType, "OrderCancelRequest", "F");
|
|
36
|
+
SetStr(msgType, "OrderCancelReplaceRequest", "G");
|
|
37
|
+
SetStr(msgType, "OrderCancelReject", "9");
|
|
38
|
+
enums.Set("MsgType", msgType);
|
|
39
|
+
|
|
40
|
+
// --- Side ---------------------------------------------------------------
|
|
41
|
+
Napi::Object side = Napi::Object::New(env);
|
|
42
|
+
SetStr(side, "Buy", "1");
|
|
43
|
+
SetStr(side, "Sell", "2");
|
|
44
|
+
SetStr(side, "SellShort", "5");
|
|
45
|
+
enums.Set("Side", side);
|
|
46
|
+
|
|
47
|
+
// --- OrdType ------------------------------------------------------------
|
|
48
|
+
Napi::Object ordType = Napi::Object::New(env);
|
|
49
|
+
SetStr(ordType, "Market", "1");
|
|
50
|
+
SetStr(ordType, "Limit", "2");
|
|
51
|
+
SetStr(ordType, "Stop", "3");
|
|
52
|
+
SetStr(ordType, "StopLimit", "4");
|
|
53
|
+
enums.Set("OrdType", ordType);
|
|
54
|
+
|
|
55
|
+
// --- TimeInForce --------------------------------------------------------
|
|
56
|
+
Napi::Object tif = Napi::Object::New(env);
|
|
57
|
+
SetStr(tif, "Day", "0");
|
|
58
|
+
SetStr(tif, "GoodTillCancel", "1");
|
|
59
|
+
SetStr(tif, "ImmediateOrCancel", "3");
|
|
60
|
+
SetStr(tif, "FillOrKill", "4");
|
|
61
|
+
enums.Set("TimeInForce", tif);
|
|
62
|
+
|
|
63
|
+
exports.Set("enums", enums);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} // namespace napi_quickfix
|
package/cpp/errors.h
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// C++ -> Napi::Error translation for the QuickFIX wrapper.
|
|
2
|
+
//
|
|
3
|
+
// Every wrapper method funnels caught exceptions through here so that JS sees a
|
|
4
|
+
// consistent Error shape:
|
|
5
|
+
// - name: "QuickFixError"
|
|
6
|
+
// - message: the exception's what()
|
|
7
|
+
// - fixError: a discriminator string. For FIX::Exception this is its `.type`
|
|
8
|
+
// (e.g. "Field not found: 35", "Configuration failed", ...) which
|
|
9
|
+
// is stable per concrete type; we ALSO set fixErrorName to the C++
|
|
10
|
+
// class name (e.g. "FieldNotFound") which is the value TS keys on.
|
|
11
|
+
// - detail: the exception's `.detail` when available.
|
|
12
|
+
//
|
|
13
|
+
// NAPI_CPP_EXCEPTIONS is enabled, so throwing a Napi::Error unwinds into a JS
|
|
14
|
+
// throw at the call boundary.
|
|
15
|
+
#ifndef NAPI_QUICKFIX_ERRORS_H
|
|
16
|
+
#define NAPI_QUICKFIX_ERRORS_H
|
|
17
|
+
|
|
18
|
+
#include <napi.h>
|
|
19
|
+
|
|
20
|
+
#include <exception>
|
|
21
|
+
#include <string>
|
|
22
|
+
|
|
23
|
+
#include "quickfix/Exceptions.h"
|
|
24
|
+
|
|
25
|
+
namespace napi_quickfix {
|
|
26
|
+
|
|
27
|
+
// Map a concrete FIX::Exception to a stable class-name discriminator. QuickFIX
|
|
28
|
+
// exceptions don't carry RTTI-friendly names, so we dynamic_cast the well-known
|
|
29
|
+
// concrete types (order: most-specific first is not required since they don't
|
|
30
|
+
// inherit from each other beyond Exception).
|
|
31
|
+
inline const char* FixErrorName(const FIX::Exception& e) {
|
|
32
|
+
if (dynamic_cast<const FIX::ConfigError*>(&e)) return "ConfigError";
|
|
33
|
+
if (dynamic_cast<const FIX::RuntimeError*>(&e)) return "RuntimeError";
|
|
34
|
+
if (dynamic_cast<const FIX::FieldNotFound*>(&e)) return "FieldNotFound";
|
|
35
|
+
if (dynamic_cast<const FIX::FieldConvertError*>(&e)) return "FieldConvertError";
|
|
36
|
+
if (dynamic_cast<const FIX::MessageParseError*>(&e)) return "MessageParseError";
|
|
37
|
+
if (dynamic_cast<const FIX::InvalidMessage*>(&e)) return "InvalidMessage";
|
|
38
|
+
if (dynamic_cast<const FIX::IncorrectDataFormat*>(&e)) return "IncorrectDataFormat";
|
|
39
|
+
if (dynamic_cast<const FIX::IncorrectTagValue*>(&e)) return "IncorrectTagValue";
|
|
40
|
+
if (dynamic_cast<const FIX::IncorrectMessageStructure*>(&e)) return "IncorrectMessageStructure";
|
|
41
|
+
if (dynamic_cast<const FIX::InvalidTagNumber*>(&e)) return "InvalidTagNumber";
|
|
42
|
+
if (dynamic_cast<const FIX::RequiredTagMissing*>(&e)) return "RequiredTagMissing";
|
|
43
|
+
if (dynamic_cast<const FIX::TagNotDefinedForMessage*>(&e)) return "TagNotDefinedForMessage";
|
|
44
|
+
if (dynamic_cast<const FIX::NoTagValue*>(&e)) return "NoTagValue";
|
|
45
|
+
if (dynamic_cast<const FIX::DuplicateFieldNumber*>(&e)) return "DuplicateFieldNumber";
|
|
46
|
+
if (dynamic_cast<const FIX::InvalidMessageType*>(&e)) return "InvalidMessageType";
|
|
47
|
+
if (dynamic_cast<const FIX::UnsupportedMessageType*>(&e)) return "UnsupportedMessageType";
|
|
48
|
+
if (dynamic_cast<const FIX::UnsupportedVersion*>(&e)) return "UnsupportedVersion";
|
|
49
|
+
if (dynamic_cast<const FIX::TagOutOfOrder*>(&e)) return "TagOutOfOrder";
|
|
50
|
+
if (dynamic_cast<const FIX::RepeatedTag*>(&e)) return "RepeatedTag";
|
|
51
|
+
if (dynamic_cast<const FIX::RepeatingGroupCountMismatch*>(&e)) return "RepeatingGroupCountMismatch";
|
|
52
|
+
if (dynamic_cast<const FIX::DoNotSend*>(&e)) return "DoNotSend";
|
|
53
|
+
if (dynamic_cast<const FIX::RejectLogon*>(&e)) return "RejectLogon";
|
|
54
|
+
if (dynamic_cast<const FIX::SessionNotFound*>(&e)) return "SessionNotFound";
|
|
55
|
+
if (dynamic_cast<const FIX::DataDictionaryNotFound*>(&e)) return "DataDictionaryNotFound";
|
|
56
|
+
if (dynamic_cast<const FIX::IOException*>(&e)) return "IOException";
|
|
57
|
+
if (dynamic_cast<const FIX::SocketException*>(&e)) return "SocketException";
|
|
58
|
+
return "FixException";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build (but do not throw) a Napi::Error from a FIX::Exception.
|
|
62
|
+
inline Napi::Error MakeFixError(Napi::Env env, const FIX::Exception& e) {
|
|
63
|
+
Napi::Error err = Napi::Error::New(env, e.what());
|
|
64
|
+
err.Set("name", Napi::String::New(env, "QuickFixError"));
|
|
65
|
+
err.Set("fixError", Napi::String::New(env, e.type));
|
|
66
|
+
err.Set("fixErrorName", Napi::String::New(env, FixErrorName(e)));
|
|
67
|
+
err.Set("detail", Napi::String::New(env, e.detail));
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build (but do not throw) a Napi::Error from a generic std::exception.
|
|
72
|
+
inline Napi::Error MakeStdError(Napi::Env env, const std::exception& e) {
|
|
73
|
+
Napi::Error err = Napi::Error::New(env, e.what());
|
|
74
|
+
err.Set("name", Napi::String::New(env, "QuickFixError"));
|
|
75
|
+
err.Set("fixErrorName", Napi::String::New(env, "Error"));
|
|
76
|
+
return err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A plain-data capture of an exception's mapped fields, safe to carry across
|
|
80
|
+
// threads (an AsyncWorker's Execute() runs off the JS thread and MUST NOT touch
|
|
81
|
+
// Napi/JS). Populate it in a catch block on the worker thread, then rebuild a
|
|
82
|
+
// Napi::Error from it in OnError/OnOK (which run on the JS thread).
|
|
83
|
+
struct CapturedError {
|
|
84
|
+
bool has = false;
|
|
85
|
+
bool isFix = false;
|
|
86
|
+
std::string message; // what()
|
|
87
|
+
std::string fixError; // FIX::Exception::type (for FIX exceptions)
|
|
88
|
+
std::string fixErrorName; // stable class-name discriminator
|
|
89
|
+
std::string detail; // FIX::Exception::detail
|
|
90
|
+
|
|
91
|
+
void Capture(const std::exception& e) {
|
|
92
|
+
has = true;
|
|
93
|
+
message = e.what();
|
|
94
|
+
if (const auto* fe = dynamic_cast<const FIX::Exception*>(&e)) {
|
|
95
|
+
isFix = true;
|
|
96
|
+
fixError = fe->type;
|
|
97
|
+
fixErrorName = FixErrorName(*fe);
|
|
98
|
+
detail = fe->detail;
|
|
99
|
+
} else {
|
|
100
|
+
isFix = false;
|
|
101
|
+
fixErrorName = "Error";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build (do not throw) a Napi::Error from the captured fields.
|
|
106
|
+
Napi::Error ToError(Napi::Env env) const {
|
|
107
|
+
Napi::Error err = Napi::Error::New(env, message);
|
|
108
|
+
err.Set("name", Napi::String::New(env, "QuickFixError"));
|
|
109
|
+
if (isFix) {
|
|
110
|
+
err.Set("fixError", Napi::String::New(env, fixError));
|
|
111
|
+
err.Set("fixErrorName", Napi::String::New(env, fixErrorName));
|
|
112
|
+
err.Set("detail", Napi::String::New(env, detail));
|
|
113
|
+
} else {
|
|
114
|
+
err.Set("fixErrorName", Napi::String::New(env, fixErrorName));
|
|
115
|
+
}
|
|
116
|
+
return err;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Translate a caught exception and throw the corresponding Napi::Error. Callers
|
|
121
|
+
// use this inside a catch block, e.g.:
|
|
122
|
+
// try { ... } catch (const std::exception& e) { TranslateAndThrow(env, e); }
|
|
123
|
+
[[noreturn]] inline void TranslateAndThrow(Napi::Env env, const std::exception& e) {
|
|
124
|
+
if (const auto* fe = dynamic_cast<const FIX::Exception*>(&e)) {
|
|
125
|
+
throw MakeFixError(env, *fe);
|
|
126
|
+
}
|
|
127
|
+
throw MakeStdError(env, e);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
} // namespace napi_quickfix
|
|
131
|
+
|
|
132
|
+
// Wrap a QuickFIX-calling body so any std::exception is converted to a JS throw.
|
|
133
|
+
// Usage:
|
|
134
|
+
// NQ_TRY(env) { ...quickfix calls that may throw... } NQ_CATCH(env)
|
|
135
|
+
// The body may `return` a Napi::Value normally.
|
|
136
|
+
#define NQ_TRY(env) try
|
|
137
|
+
#define NQ_CATCH(env) \
|
|
138
|
+
catch (const std::exception& _nq_e) { \
|
|
139
|
+
::napi_quickfix::TranslateAndThrow((env), _nq_e); \
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#endif // NAPI_QUICKFIX_ERRORS_H
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#include "initiator_wrap.h"
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
#include "quickfix/FileLog.h"
|
|
6
|
+
#include "quickfix/FileStore.h"
|
|
7
|
+
|
|
8
|
+
#include "engine_common.h"
|
|
9
|
+
#include "engine_workers.h"
|
|
10
|
+
#include "errors.h"
|
|
11
|
+
#include "session_settings_wrap.h"
|
|
12
|
+
|
|
13
|
+
namespace napi_quickfix {
|
|
14
|
+
|
|
15
|
+
Napi::FunctionReference InitiatorWrap::constructor_;
|
|
16
|
+
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
std::string OptString(Napi::Object opts, const char* key,
|
|
20
|
+
const std::string& dflt) {
|
|
21
|
+
if (opts.Has(key)) {
|
|
22
|
+
Napi::Value v = opts.Get(key);
|
|
23
|
+
if (v.IsString()) return v.As<Napi::String>().Utf8Value();
|
|
24
|
+
}
|
|
25
|
+
return dflt;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
} // namespace
|
|
29
|
+
|
|
30
|
+
Napi::Object InitiatorWrap::Init(Napi::Env env, Napi::Object exports) {
|
|
31
|
+
Napi::Function func = DefineClass(
|
|
32
|
+
env, "Initiator",
|
|
33
|
+
{
|
|
34
|
+
InstanceMethod("start", &InitiatorWrap::Start),
|
|
35
|
+
InstanceMethod("stop", &InitiatorWrap::Stop),
|
|
36
|
+
InstanceMethod("isLoggedOn", &InitiatorWrap::IsLoggedOn),
|
|
37
|
+
InstanceMethod("ref", &InitiatorWrap::Ref),
|
|
38
|
+
InstanceMethod("unref", &InitiatorWrap::Unref),
|
|
39
|
+
});
|
|
40
|
+
constructor_ = Napi::Persistent(func);
|
|
41
|
+
constructor_.SuppressDestruct();
|
|
42
|
+
exports.Set("InitiatorWrap", func);
|
|
43
|
+
return exports;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
InitiatorWrap::InitiatorWrap(const Napi::CallbackInfo& info)
|
|
47
|
+
: Napi::ObjectWrap<InitiatorWrap>(info) {
|
|
48
|
+
Napi::Env env = info.Env();
|
|
49
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
50
|
+
throw Napi::TypeError::New(
|
|
51
|
+
env, "new Initiator({ settings, handlers?, store?, log? })");
|
|
52
|
+
}
|
|
53
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
54
|
+
|
|
55
|
+
SessionSettingsWrap* settingsWrap =
|
|
56
|
+
SessionSettingsWrap::UnwrapArg(env, opts.Get("settings"), "settings");
|
|
57
|
+
|
|
58
|
+
Napi::Object handlers;
|
|
59
|
+
if (opts.Has("handlers") && opts.Get("handlers").IsObject()) {
|
|
60
|
+
handlers = opts.Get("handlers").As<Napi::Object>();
|
|
61
|
+
} else {
|
|
62
|
+
handlers = Napi::Object::New(env);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
std::string store = OptString(opts, "store", "file");
|
|
66
|
+
std::string log = OptString(opts, "log", "screen");
|
|
67
|
+
|
|
68
|
+
const FIX::SessionSettings& settings = settingsWrap->Settings();
|
|
69
|
+
|
|
70
|
+
// Bridge must be created on the JS thread (captures env / handlers).
|
|
71
|
+
bridge_ = std::make_unique<ApplicationBridge>(env, handlers);
|
|
72
|
+
|
|
73
|
+
NQ_TRY(env) {
|
|
74
|
+
if (store == "memory") {
|
|
75
|
+
storeFactory_ = std::make_unique<FIX::MemoryStoreFactory>();
|
|
76
|
+
} else {
|
|
77
|
+
storeFactory_ = std::make_unique<FIX::FileStoreFactory>(settings);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (log == "none") {
|
|
81
|
+
logFactory_ = std::make_unique<NullLogFactory>();
|
|
82
|
+
} else if (log == "file") {
|
|
83
|
+
logFactory_ = std::make_unique<FIX::FileLogFactory>(settings);
|
|
84
|
+
} else {
|
|
85
|
+
logFactory_ = std::make_unique<FIX::ScreenLogFactory>(settings);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
initiator_ = std::make_unique<FIX::SocketInitiator>(
|
|
89
|
+
*bridge_, *storeFactory_, settings, *logFactory_);
|
|
90
|
+
} NQ_CATCH(env)
|
|
91
|
+
|
|
92
|
+
// Register a cleanup hook so teardown runs deterministically on the MAIN
|
|
93
|
+
// thread during environment shutdown, BEFORE the TSFN is finalized. ObjectWrap
|
|
94
|
+
// finalizers do NOT reliably run before env teardown when the loop is
|
|
95
|
+
// unref'd, so relying on the destructor alone lets a live QuickFIX network
|
|
96
|
+
// thread call into an already-destroyed TSFN and abort the process.
|
|
97
|
+
env_ = env;
|
|
98
|
+
cleanupHook_ = env.AddCleanupHook(&InitiatorWrap::CleanupEntry, this);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
void InitiatorWrap::CleanupEntry(InitiatorWrap* self) {
|
|
102
|
+
// Runs on the MAIN thread during env shutdown. Join network threads and tear
|
|
103
|
+
// down the bridge before N-API destroys the TSFN. Mark fired so the later
|
|
104
|
+
// destructor doesn't Remove() an already-freed cleanup hook (double free).
|
|
105
|
+
self->cleanupHookFired_ = true;
|
|
106
|
+
self->Teardown(true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
InitiatorWrap::~InitiatorWrap() {
|
|
110
|
+
// If the object is GC-finalized BEFORE env shutdown, remove the cleanup hook
|
|
111
|
+
// so it won't fire on freed memory. If the hook already fired, node already
|
|
112
|
+
// freed the hook data — do NOT Remove() (that would double-free).
|
|
113
|
+
if (!cleanupHookFired_ && !cleanupHook_.IsEmpty() && env_ != nullptr) {
|
|
114
|
+
cleanupHook_.Remove(env_);
|
|
115
|
+
}
|
|
116
|
+
Teardown(true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
void InitiatorWrap::Teardown(bool force) {
|
|
120
|
+
// Runs on the MAIN thread (finalizer / explicit teardown). MUST NOT block on a
|
|
121
|
+
// JS round-trip and MUST NOT deadlock. Ordering matters:
|
|
122
|
+
// 1. Deactivate the bridge (set a flag, no TSFN op): any callback firing on
|
|
123
|
+
// a QuickFIX network thread during the upcoming stop() will pass through
|
|
124
|
+
// immediately without touching the TSFN or waiting on the (frozen) JS
|
|
125
|
+
// loop. This breaks the stop()->onLogout->BlockingCall deadlock.
|
|
126
|
+
// 2. stop(force): joins all QuickFIX network threads. After it returns, no
|
|
127
|
+
// network thread is running, so none can race into the TSFN.
|
|
128
|
+
// 3. Abort the TSFN: safe now that no producer thread remains.
|
|
129
|
+
if (stopped_) return;
|
|
130
|
+
stopped_ = true;
|
|
131
|
+
if (bridge_) {
|
|
132
|
+
bridge_->Deactivate();
|
|
133
|
+
}
|
|
134
|
+
if (initiator_ && started_) {
|
|
135
|
+
try {
|
|
136
|
+
initiator_->stop(force);
|
|
137
|
+
} catch (...) {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (bridge_) {
|
|
141
|
+
bridge_->Abort();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Napi::Value InitiatorWrap::Start(const Napi::CallbackInfo& info) {
|
|
146
|
+
Napi::Env env = info.Env();
|
|
147
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
148
|
+
if (stopped_) {
|
|
149
|
+
deferred.Reject(Napi::Error::New(env, "Initiator has been stopped").Value());
|
|
150
|
+
return deferred.Promise();
|
|
151
|
+
}
|
|
152
|
+
if (started_) {
|
|
153
|
+
deferred.Resolve(env.Undefined());
|
|
154
|
+
return deferred.Promise();
|
|
155
|
+
}
|
|
156
|
+
if (busy_) {
|
|
157
|
+
deferred.Reject(
|
|
158
|
+
Napi::Error::New(env, "Initiator start/stop already in progress")
|
|
159
|
+
.Value());
|
|
160
|
+
return deferred.Promise();
|
|
161
|
+
}
|
|
162
|
+
busy_ = true;
|
|
163
|
+
started_ = true; // set optimistically; reset in onSettled on failure
|
|
164
|
+
|
|
165
|
+
FIX::SocketInitiator* initiator = initiator_.get();
|
|
166
|
+
auto* self = this;
|
|
167
|
+
auto* worker = new EngineOpWorker(
|
|
168
|
+
env, info.This().As<Napi::Object>(),
|
|
169
|
+
[initiator]() { initiator->start(); },
|
|
170
|
+
[self](bool ok) {
|
|
171
|
+
self->busy_ = false;
|
|
172
|
+
// If start failed, mark not-started so teardown won't try to stop an
|
|
173
|
+
// engine that never came up, and a later start() can retry.
|
|
174
|
+
if (!ok) self->started_ = false;
|
|
175
|
+
});
|
|
176
|
+
Napi::Promise promise = worker->Promise();
|
|
177
|
+
worker->Queue();
|
|
178
|
+
return promise;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Napi::Value InitiatorWrap::Stop(const Napi::CallbackInfo& info) {
|
|
182
|
+
Napi::Env env = info.Env();
|
|
183
|
+
bool force = false;
|
|
184
|
+
if (info.Length() >= 1 && !info[0].IsUndefined()) {
|
|
185
|
+
force = info[0].ToBoolean().Value();
|
|
186
|
+
}
|
|
187
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
188
|
+
if (stopped_) {
|
|
189
|
+
deferred.Resolve(env.Undefined());
|
|
190
|
+
return deferred.Promise();
|
|
191
|
+
}
|
|
192
|
+
if (busy_) {
|
|
193
|
+
deferred.Reject(
|
|
194
|
+
Napi::Error::New(env, "Initiator start/stop already in progress")
|
|
195
|
+
.Value());
|
|
196
|
+
return deferred.Promise();
|
|
197
|
+
}
|
|
198
|
+
if (!initiator_ || !started_) {
|
|
199
|
+
// Never started (or already torn down): just release the TSFN and resolve.
|
|
200
|
+
stopped_ = true;
|
|
201
|
+
if (bridge_) bridge_->Release();
|
|
202
|
+
deferred.Resolve(env.Undefined());
|
|
203
|
+
return deferred.Promise();
|
|
204
|
+
}
|
|
205
|
+
busy_ = true;
|
|
206
|
+
stopped_ = true;
|
|
207
|
+
|
|
208
|
+
FIX::SocketInitiator* initiator = initiator_.get();
|
|
209
|
+
ApplicationBridge* bridge = bridge_.get();
|
|
210
|
+
auto* self = this;
|
|
211
|
+
auto* worker = new EngineOpWorker(
|
|
212
|
+
env, info.This().As<Napi::Object>(),
|
|
213
|
+
// stop() runs on the worker thread; its onLogout/toAdmin callbacks fire on
|
|
214
|
+
// QuickFIX threads while the main loop is free to service the TSFN.
|
|
215
|
+
[initiator, force]() { initiator->stop(force); },
|
|
216
|
+
[self, bridge, force](bool /*ok*/) {
|
|
217
|
+
self->busy_ = false;
|
|
218
|
+
// Now that stop() has completed and no more callbacks will fire, drain
|
|
219
|
+
// (or abort) the TSFN so the event loop can exit on its own.
|
|
220
|
+
if (bridge) {
|
|
221
|
+
if (force) {
|
|
222
|
+
bridge->Abort();
|
|
223
|
+
} else {
|
|
224
|
+
bridge->Release();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
Napi::Promise promise = worker->Promise();
|
|
229
|
+
worker->Queue();
|
|
230
|
+
return promise;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Napi::Value InitiatorWrap::IsLoggedOn(const Napi::CallbackInfo& info) {
|
|
234
|
+
Napi::Env env = info.Env();
|
|
235
|
+
if (!initiator_ || !started_ || stopped_) {
|
|
236
|
+
return Napi::Boolean::New(env, false);
|
|
237
|
+
}
|
|
238
|
+
NQ_TRY(env) {
|
|
239
|
+
return Napi::Boolean::New(env, initiator_->isLoggedOn());
|
|
240
|
+
} NQ_CATCH(env)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Napi::Value InitiatorWrap::Ref(const Napi::CallbackInfo& info) {
|
|
244
|
+
Napi::Env env = info.Env();
|
|
245
|
+
if (bridge_) bridge_->Ref(env);
|
|
246
|
+
return info.This();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Napi::Value InitiatorWrap::Unref(const Napi::CallbackInfo& info) {
|
|
250
|
+
Napi::Env env = info.Env();
|
|
251
|
+
if (bridge_) bridge_->Unref(env);
|
|
252
|
+
return info.This();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
} // namespace napi_quickfix
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#ifndef NAPI_QUICKFIX_INITIATOR_WRAP_H
|
|
2
|
+
#define NAPI_QUICKFIX_INITIATOR_WRAP_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#include <memory>
|
|
7
|
+
|
|
8
|
+
#include "quickfix/Log.h"
|
|
9
|
+
#include "quickfix/MessageStore.h"
|
|
10
|
+
#include "quickfix/SocketInitiator.h"
|
|
11
|
+
|
|
12
|
+
#include "application_bridge.h"
|
|
13
|
+
|
|
14
|
+
namespace napi_quickfix {
|
|
15
|
+
|
|
16
|
+
// Napi::ObjectWrap over FIX::SocketInitiator.
|
|
17
|
+
//
|
|
18
|
+
// new Initiator({ settings, handlers?, store?, log? })
|
|
19
|
+
// store: 'file' | 'memory' (default 'file')
|
|
20
|
+
// log: 'screen' | 'file' | 'none' (default 'screen')
|
|
21
|
+
//
|
|
22
|
+
// Methods: start / stop(force?) / isLoggedOn / ref / unref.
|
|
23
|
+
class InitiatorWrap : public Napi::ObjectWrap<InitiatorWrap> {
|
|
24
|
+
public:
|
|
25
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
26
|
+
explicit InitiatorWrap(const Napi::CallbackInfo& info);
|
|
27
|
+
~InitiatorWrap() override;
|
|
28
|
+
|
|
29
|
+
private:
|
|
30
|
+
static Napi::FunctionReference constructor_;
|
|
31
|
+
|
|
32
|
+
Napi::Value Start(const Napi::CallbackInfo& info);
|
|
33
|
+
Napi::Value Stop(const Napi::CallbackInfo& info);
|
|
34
|
+
Napi::Value IsLoggedOn(const Napi::CallbackInfo& info);
|
|
35
|
+
Napi::Value Ref(const Napi::CallbackInfo& info);
|
|
36
|
+
Napi::Value Unref(const Napi::CallbackInfo& info);
|
|
37
|
+
|
|
38
|
+
void Teardown(bool force);
|
|
39
|
+
|
|
40
|
+
// Declaration order matters: members destruct in REVERSE order, so the
|
|
41
|
+
// initiator (declared last) is destroyed FIRST, before the factories and
|
|
42
|
+
// bridge it references.
|
|
43
|
+
std::unique_ptr<ApplicationBridge> bridge_;
|
|
44
|
+
std::unique_ptr<FIX::MessageStoreFactory> storeFactory_;
|
|
45
|
+
std::unique_ptr<FIX::LogFactory> logFactory_;
|
|
46
|
+
std::unique_ptr<FIX::SocketInitiator> initiator_;
|
|
47
|
+
|
|
48
|
+
bool started_ = false;
|
|
49
|
+
bool stopped_ = false;
|
|
50
|
+
bool busy_ = false; // guards against overlapping start/stop AsyncWorkers
|
|
51
|
+
|
|
52
|
+
static void CleanupEntry(InitiatorWrap* self);
|
|
53
|
+
|
|
54
|
+
// Env captured at construction (JS thread), used only to remove the cleanup
|
|
55
|
+
// hook if the object is GC-finalized before environment shutdown.
|
|
56
|
+
napi_env env_ = nullptr;
|
|
57
|
+
// Runs Teardown on the MAIN thread during environment shutdown, BEFORE N-API
|
|
58
|
+
// finalizes the TSFN — the only reliable point to join QuickFIX network
|
|
59
|
+
// threads before the TSFN they call into is destroyed. Removed in the
|
|
60
|
+
// destructor ONLY if the hook has not already fired (node's cleanup wrapper
|
|
61
|
+
// frees its own data after invoking the hook, so Remove() afterward would
|
|
62
|
+
// double-free; we track that with cleanupHookFired_).
|
|
63
|
+
Napi::Env::CleanupHook<void (*)(InitiatorWrap*), InitiatorWrap> cleanupHook_;
|
|
64
|
+
bool cleanupHookFired_ = false;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
} // namespace napi_quickfix
|
|
68
|
+
|
|
69
|
+
#endif // NAPI_QUICKFIX_INITIATOR_WRAP_H
|