@homaiohq/napi-quickfix 0.0.0-stage → 0.2.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 (104) hide show
  1. package/CMakeLists.txt +135 -0
  2. package/LICENSE +21 -0
  3. package/README.md +638 -2
  4. package/THIRD_PARTY_LICENSES +48 -0
  5. package/cpp/acceptor_wrap.cpp +468 -0
  6. package/cpp/acceptor_wrap.h +116 -0
  7. package/cpp/addon.cpp +87 -0
  8. package/cpp/application_bridge.cpp +466 -0
  9. package/cpp/application_bridge.h +204 -0
  10. package/cpp/data_dictionary_wrap.cpp +165 -0
  11. package/cpp/data_dictionary_wrap.h +59 -0
  12. package/cpp/engine_common.cpp +5 -0
  13. package/cpp/engine_common.h +127 -0
  14. package/cpp/engine_workers.h +167 -0
  15. package/cpp/errors.h +142 -0
  16. package/cpp/field_map_ops.h +492 -0
  17. package/cpp/group_wrap.cpp +221 -0
  18. package/cpp/group_wrap.h +76 -0
  19. package/cpp/initiator_wrap.cpp +492 -0
  20. package/cpp/initiator_wrap.h +104 -0
  21. package/cpp/message_wrap.cpp +288 -0
  22. package/cpp/message_wrap.h +95 -0
  23. package/cpp/session_id_wrap.cpp +91 -0
  24. package/cpp/session_id_wrap.h +46 -0
  25. package/cpp/session_op_gate.h +201 -0
  26. package/cpp/session_settings_wrap.cpp +98 -0
  27. package/cpp/session_settings_wrap.h +40 -0
  28. package/cpp/session_static.cpp +220 -0
  29. package/cpp/session_wrap.cpp +438 -0
  30. package/cpp/session_wrap.h +144 -0
  31. package/dist/cjs/acceptor.d.ts +29 -0
  32. package/dist/cjs/acceptor.js +38 -0
  33. package/dist/cjs/application.d.ts +115 -0
  34. package/dist/cjs/application.js +62 -0
  35. package/dist/cjs/data-dictionary.d.ts +67 -0
  36. package/dist/cjs/data-dictionary.js +105 -0
  37. package/dist/cjs/engine.d.ts +152 -0
  38. package/dist/cjs/engine.js +183 -0
  39. package/dist/cjs/enums.d.ts +56 -0
  40. package/dist/cjs/enums.js +44 -0
  41. package/dist/cjs/field-value.d.ts +12 -0
  42. package/dist/cjs/field-value.js +40 -0
  43. package/dist/cjs/generated/fields.d.ts +6122 -0
  44. package/dist/cjs/generated/fields.js +6127 -0
  45. package/dist/cjs/generated/values.d.ts +8548 -0
  46. package/dist/cjs/generated/values.js +8580 -0
  47. package/dist/cjs/group.d.ts +126 -0
  48. package/dist/cjs/group.js +185 -0
  49. package/dist/cjs/index.d.ts +68 -0
  50. package/dist/cjs/index.js +73 -0
  51. package/dist/cjs/initiator.d.ts +29 -0
  52. package/dist/cjs/initiator.js +38 -0
  53. package/dist/cjs/load-native.cjs +5 -0
  54. package/dist/cjs/message.d.ts +238 -0
  55. package/dist/cjs/message.js +299 -0
  56. package/dist/cjs/native.d.ts +325 -0
  57. package/dist/cjs/native.js +22 -0
  58. package/dist/cjs/package.json +1 -0
  59. package/dist/cjs/session-id.d.ts +51 -0
  60. package/dist/cjs/session-id.js +87 -0
  61. package/dist/cjs/session-settings.d.ts +46 -0
  62. package/dist/cjs/session-settings.js +75 -0
  63. package/dist/cjs/session.d.ts +213 -0
  64. package/dist/cjs/session.js +337 -0
  65. package/dist/esm/acceptor.d.ts +29 -0
  66. package/dist/esm/acceptor.js +34 -0
  67. package/dist/esm/application.d.ts +115 -0
  68. package/dist/esm/application.js +57 -0
  69. package/dist/esm/data-dictionary.d.ts +67 -0
  70. package/dist/esm/data-dictionary.js +101 -0
  71. package/dist/esm/engine.d.ts +152 -0
  72. package/dist/esm/engine.js +179 -0
  73. package/dist/esm/enums.d.ts +56 -0
  74. package/dist/esm/enums.js +35 -0
  75. package/dist/esm/field-value.d.ts +12 -0
  76. package/dist/esm/field-value.js +37 -0
  77. package/dist/esm/generated/fields.d.ts +6122 -0
  78. package/dist/esm/generated/fields.js +6124 -0
  79. package/dist/esm/generated/values.d.ts +8548 -0
  80. package/dist/esm/generated/values.js +8564 -0
  81. package/dist/esm/group.d.ts +126 -0
  82. package/dist/esm/group.js +181 -0
  83. package/dist/esm/index.d.ts +68 -0
  84. package/dist/esm/index.js +43 -0
  85. package/dist/esm/initiator.d.ts +29 -0
  86. package/dist/esm/initiator.js +34 -0
  87. package/dist/esm/load-native.cjs +5 -0
  88. package/dist/esm/message.d.ts +238 -0
  89. package/dist/esm/message.js +293 -0
  90. package/dist/esm/native.d.ts +325 -0
  91. package/dist/esm/native.js +16 -0
  92. package/dist/esm/session-id.d.ts +51 -0
  93. package/dist/esm/session-id.js +82 -0
  94. package/dist/esm/session-settings.d.ts +46 -0
  95. package/dist/esm/session-settings.js +71 -0
  96. package/dist/esm/session.d.ts +213 -0
  97. package/dist/esm/session.js +329 -0
  98. package/package.json +95 -4
  99. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  100. package/prebuilds/darwin-x64/node.napi.node +0 -0
  101. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  102. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  103. package/prebuilds/win32-x64/node.napi.node +0 -0
  104. package/src/load-native.cjs +5 -0
package/README.md CHANGED
@@ -1,3 +1,639 @@
1
- # Temporary Holding Version
1
+ # @homaiohq/napi-quickfix
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/homaiohq/napi-quickfix/actions/workflows/ci.yml/badge.svg)](https://github.com/homaiohq/napi-quickfix/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@homaiohq/napi-quickfix.svg)](https://www.npmjs.com/package/@homaiohq/napi-quickfix)
5
+ [![Node.js](https://img.shields.io/node/v/@homaiohq/napi-quickfix.svg)](https://nodejs.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
+
8
+ A modern **Node-API** (N-API / `node-addon-api`) C++ binding for the
9
+ [QuickFIX](https://github.com/quickfix/quickfix) FIX-protocol engine. It exposes
10
+ both the *pure* message layer (build, parse, and validate FIX messages) and the
11
+ full *session engine* (socket initiator/acceptor with `FIX::Application`
12
+ callbacks bridged to JavaScript) — with prebuilt binaries so most consumers need
13
+ **no C++ compiler**, dual **ESM + CommonJS** entry points, and TypeScript types
14
+ included.
15
+
16
+ ## Features
17
+
18
+ - **Modern Node-API** — built on `node-addon-api` (N-API v9), not the legacy
19
+ NAN/node-gyp stack. One prebuild per platform serves every supported Node LTS.
20
+ - **No compiler needed** — prebuilt native binaries ship in the npm package (via
21
+ `prebuildify` + `node-gyp-build`); a source build via CMake is only a fallback.
22
+ - **Dual ESM + CJS** — a proper `exports` map with `import` and `require` entry
23
+ points.
24
+ - **TypeScript types included** — hand-written `.d.ts` for the full public API.
25
+ - **Full FIX surface** — the session engine (`Initiator`/`Acceptor` +
26
+ synchronous application handlers), per-session control (`Session`: sequence
27
+ numbers, logon/logout, runtime options) *and* the pure layer (`Message` with
28
+ repeating `Group`s and dictionary-aware parsing, `SessionSettings`,
29
+ `DataDictionary`, `SessionID`).
30
+ - **Ergonomic, safe API** — chainable message building with automatic
31
+ header/trailer field routing, typed rejections, and C++ exceptions surfaced as
32
+ JavaScript `Error`s.
33
+ - **Every FIX constant, typed** — `FIELD` (tag numbers) and one value group per
34
+ field (`MsgType`, `Side`, `ExecType`, `OrdStatus`, ...) are
35
+ generated from the bundled QuickFIX headers with literal types, frozen, and
36
+ importable without loading the native addon.
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ yarn add @homaiohq/napi-quickfix
42
+ # or
43
+ npm install @homaiohq/napi-quickfix
44
+ # or
45
+ pnpm add @homaiohq/napi-quickfix
46
+ ```
47
+
48
+ Prebuilt binaries are published for **Linux x64** (both **glibc** and
49
+ **musl/Alpine**), **macOS** x64 + arm64, and **Windows** x64. On these platforms
50
+ installation requires no toolchain.
51
+
52
+ The two Linux binaries are libc-tagged and picked automatically: `node-gyp-build`
53
+ selects the musl build when `/etc/alpine-release` exists. On a musl distro that
54
+ is *not* Alpine, set `LIBC=musl` to force it.
55
+
56
+ If a matching prebuild is not available for your platform/arch, the install
57
+ falls back to building from source, which requires:
58
+
59
+ - **CMake** (>= 3.12)
60
+ - A **C++17** compiler (GCC / Clang / MSVC)
61
+
62
+ On Alpine/musl that is `apk add build-base cmake git` — `build-base` supplies
63
+ g++/make, and `git` is needed for the QuickFIX `FetchContent` clone.
64
+
65
+ QuickFIX itself is fetched at build time via CMake `FetchContent` (pinned to
66
+ `v1.16.0`) — nothing is vendored or committed to this repository.
67
+
68
+ Releases are published from GitHub Actions via npm Trusted Publishing with
69
+ [provenance](https://docs.npmjs.com/generating-provenance-statements), so you can
70
+ check that an installed version was built from this repository:
71
+
72
+ ```sh
73
+ npm audit signatures
74
+ ```
75
+
76
+ ## Quick start
77
+
78
+ ### ESM
79
+
80
+ ```ts
81
+ import {
82
+ Message,
83
+ SessionSettings,
84
+ Initiator,
85
+ FixReject,
86
+ sendToTarget,
87
+ FIELD,
88
+ MsgType,
89
+ } from '@homaiohq/napi-quickfix';
90
+
91
+ // Build a message. Header/trailer fields (e.g. MsgType) auto-route to the right
92
+ // section, so setField is all you usually need. setField is chainable.
93
+ const order = new Message()
94
+ .setField(FIELD.MsgType, MsgType.NewOrderSingle)
95
+ .setField(FIELD.ClOrdID, 'order-1')
96
+ .setField(FIELD.Symbol, 'AAPL')
97
+ .setField(FIELD.OrderQty, 100);
98
+
99
+ console.log(order.getMsgType()); // 'D'
100
+ console.log(order.toPretty());
101
+
102
+ // Parse a raw FIX wire string back into a Message.
103
+ const parsed = Message.parse(order.toString());
104
+
105
+ // Load session settings from an in-memory config.
106
+ const settings = SessionSettings.fromString(`
107
+ [DEFAULT]
108
+ ConnectionType=initiator
109
+ SocketConnectHost=127.0.0.1
110
+ SocketConnectPort=5001
111
+ HeartBtInt=30
112
+ UseDataDictionary=N
113
+ StartTime=00:00:00
114
+ EndTime=00:00:00
115
+
116
+ [SESSION]
117
+ BeginString=FIX.4.4
118
+ SenderCompID=CLIENT
119
+ TargetCompID=BROKER
120
+ `);
121
+
122
+ // Create the initiator with synchronous application handlers.
123
+ const initiator = new Initiator({
124
+ settings,
125
+ store: 'memory',
126
+ log: 'none',
127
+ handlers: {
128
+ onLogon(sessionID) {
129
+ console.log('logged on:', sessionID.toString());
130
+ // sessionID.toString() -> "FIX.4.4:CLIENT->BROKER"
131
+ sendToTarget(order, sessionID);
132
+ },
133
+ toAdmin(msg) {
134
+ // Handlers run synchronously and may MUTATE the outbound message.
135
+ if (msg.getMsgType() === MsgType.Logon) {
136
+ msg.setField(FIELD.Username, 'user');
137
+ msg.setField(FIELD.Password, 'secret');
138
+ }
139
+ },
140
+ fromApp(msg) {
141
+ // Reject an inbound message by THROWING a typed rejection.
142
+ if (msg.getMsgType() === 'X') {
143
+ throw new FixReject('UnsupportedMessageType');
144
+ }
145
+ console.log('received:', msg.getMsgType());
146
+ },
147
+ },
148
+ });
149
+
150
+ // Observe-only events (cannot mutate/reject) are also emitted.
151
+ initiator.on('logout', (id) => console.log('logged out:', id.toString()));
152
+
153
+ initiator.start();
154
+
155
+ // ... later, shut down cleanly (drain gracefully).
156
+ process.on('SIGINT', () => {
157
+ initiator.stop();
158
+ });
159
+ ```
160
+
161
+ ### CommonJS
162
+
163
+ ```js
164
+ const {
165
+ Message,
166
+ SessionSettings,
167
+ Initiator,
168
+ FIELD,
169
+ MsgType,
170
+ } = require('@homaiohq/napi-quickfix');
171
+
172
+ const order = new Message()
173
+ .setField(FIELD.MsgType, MsgType.NewOrderSingle)
174
+ .setField(FIELD.ClOrdID, 'order-1')
175
+ .setField(FIELD.Symbol, 'AAPL');
176
+
177
+ const settings = SessionSettings.fromString(`
178
+ [DEFAULT]
179
+ ConnectionType=initiator
180
+ SocketConnectHost=127.0.0.1
181
+ SocketConnectPort=5001
182
+ HeartBtInt=30
183
+ UseDataDictionary=N
184
+ StartTime=00:00:00
185
+ EndTime=00:00:00
186
+
187
+ [SESSION]
188
+ BeginString=FIX.4.4
189
+ SenderCompID=CLIENT
190
+ TargetCompID=BROKER
191
+ `);
192
+
193
+ const initiator = new Initiator({
194
+ settings,
195
+ store: 'memory',
196
+ log: 'none',
197
+ handlers: {
198
+ onLogon: (id) => console.log('logged on:', id.toString()),
199
+ },
200
+ });
201
+
202
+ initiator.start();
203
+ // initiator.stop() when done.
204
+ ```
205
+
206
+ ## API overview
207
+
208
+ The full type surface is documented in the shipped TypeScript declarations
209
+ (`dist/esm/index.d.ts`); the reference below is a concise summary. All exports
210
+ come from `@homaiohq/napi-quickfix`.
211
+
212
+ ### Message
213
+
214
+ ```ts
215
+ new Message(raw?: string, opts?: MessageParseOptions);
216
+ Message.parse(raw: string, opts?: MessageParseOptions): Message;
217
+ // MessageParseOptions: { validate?: boolean; dictionary?: DataDictionary;
218
+ // sessionDictionary?: DataDictionary; applicationDictionary?: DataDictionary }
219
+
220
+ message.setField(tag: number, value: string | number): this; // chainable, auto-routes header/trailer fields
221
+ message.getField(tag: number): string; // throws QuickFixError (FieldNotFound) if absent
222
+ // Standard header/trailer tags route to their section. Any other tag is a body field, unless the
223
+ // header or trailer already holds it (a field a dictionary declares there): then it is read and
224
+ // edited in place. A new dictionary-declared header field goes in with setHeaderField.
225
+ message.getFieldIfSet(tag: number): string | undefined;
226
+ message.isSetField(tag: number): boolean;
227
+ message.removeField(tag: number): this;
228
+ message.setHeaderField(tag, value): this; message.getHeaderField(tag): string;
229
+ message.setTrailerField(tag, value): this; message.getTrailerField(tag): string;
230
+ message.fields(): [tag: number, value: string][]; // body fields in wire order (also iterable)
231
+ message.headerFields(); message.trailerFields(); // same shape
232
+ message.isEmpty(): boolean; message.totalFields(): number; message.clear(): this;
233
+
234
+ // Tags must be integers (positive for setters) and group indices integers: anything else throws a TypeError.
235
+ // Repeating groups (indices are 1-based, as in QuickFIX)
236
+ message.addGroup(group: Group): this; // appends a copy, maintains the count field; TypeError if group.delim differs from the stored entries'
237
+ message.getGroup(index: number, tag: number): Group; // snapshot copy; throws FieldNotFound if absent
238
+ message.replaceGroup(index: number, group: Group): this; // throws FieldNotFound if absent, TypeError if group.delim differs
239
+ message.removeGroup(tag: number): this; message.removeGroup(index: number, tag: number): this; // (index, tag) throws FieldNotFound if absent
240
+ message.hasGroup(tag: number): boolean; message.hasGroup(index: number, tag: number): boolean;
241
+ message.groupCount(tag: number): number;
242
+
243
+ message.getMsgType(): string;
244
+ message.toString(): string; // raw SOH-delimited wire string
245
+ message.toPretty(): string; // human-readable
246
+ message.toJSON(): { msgType?: string; raw: string };
247
+
248
+ createMessage(fields?: Record<number, string | number>): Message;
249
+ parseMessage(raw: string, opts?: MessageParseOptions): Message;
250
+ ```
251
+
252
+ Field values are accepted as `string | number` (numbers are stringified) and
253
+ always returned as `string`. Header/trailer fields such as `MsgType`,
254
+ `BeginString`, and `CheckSum` are **automatically routed** to the correct
255
+ section by `setField`, so you rarely need `setHeaderField`/`setTrailerField`
256
+ directly.
257
+
258
+ **Parsing and repeating groups.** A parse without a dictionary is *flat*: the
259
+ fields of a repeating group are kept as plain repeated body fields and
260
+ `getGroup` throws. Pass a `dictionary` (or a `sessionDictionary` /
261
+ `applicationDictionary` pair for FIXT/FIX 5) to parse groups structurally.
262
+ With `UseDataDictionary=Y` (the default), messages delivered to engine handlers
263
+ are already parsed with the session's dictionary, so `getGroup` works on them
264
+ directly; with `UseDataDictionary=N` they arrive flat.
265
+
266
+ ```ts
267
+ import { DataDictionary, Group, Message, FIELD, MsgType } from '@homaiohq/napi-quickfix';
268
+
269
+ const dd = DataDictionary.fromFile('./spec/FIX44.xml');
270
+
271
+ const order = new Message()
272
+ .setField(FIELD.MsgType, MsgType.NewOrderSingle)
273
+ .setField(FIELD.ClOrdID, 'order-1')
274
+ .addGroup(new Group(FIELD.NoPartyIDs, FIELD.PartyID).setField(FIELD.PartyID, 'TRADER-1').setField(FIELD.PartyRole, 11))
275
+ .addGroup(new Group(FIELD.NoPartyIDs, FIELD.PartyID).setField(FIELD.PartyID, 'FIRM-7').setField(FIELD.PartyRole, 1));
276
+ // 453=2|448=TRADER-1|452=11|448=FIRM-7|452=1 on the wire
277
+
278
+ const parsed = Message.parse(order.toString(), { dictionary: dd });
279
+ parsed.groupCount(FIELD.NoPartyIDs); // 2
280
+ parsed.getGroup(2, FIELD.NoPartyIDs).getField(FIELD.PartyID); // 'FIRM-7'
281
+
282
+ // getGroup returns a snapshot: write changes back with replaceGroup.
283
+ const first = parsed.getGroup(1, FIELD.NoPartyIDs).setField(FIELD.PartyRole, 12);
284
+ parsed.replaceGroup(1, first);
285
+ ```
286
+
287
+ ### Group
288
+
289
+ One entry of a repeating group, identified by its **count tag** (`field`, e.g.
290
+ `453 NoPartyIDs`) and its **delimiter** (`delim`, the first tag of every entry,
291
+ e.g. `448 PartyID`). Groups nest through the same `addGroup`/`getGroup` API.
292
+
293
+ ```ts
294
+ new Group(field: number, delim: number, order?: number[]); // order: full entry field order, delimiter first, no tag twice
295
+ // delim and order tags are capped at 2^20 (RangeError beyond that)
296
+ group.field; group.delim; // read-only
297
+
298
+ group.setField(tag, value: string | number): this; group.getField(tag): string;
299
+ group.getFieldIfSet(tag): string | undefined; group.isSetField(tag): boolean;
300
+ group.removeField(tag): this; group.isEmpty(); group.totalFields(); group.clear(): this;
301
+ group.fields(): [tag, value][]; // in wire order (also iterable)
302
+ group.toString(): string; // the entry's fields, SOH-delimited, in wire order
303
+
304
+ // Nested groups — same contract as on Message
305
+ group.addGroup(g: Group): this; group.getGroup(index, tag): Group; group.replaceGroup(index, g): this;
306
+ group.removeGroup(tag): this; group.removeGroup(index, tag): this;
307
+ group.hasGroup(tag): boolean; group.hasGroup(index, tag): boolean; group.groupCount(tag): number;
308
+ ```
309
+
310
+ Without an explicit `order`, an entry's fields are emitted delimiter-first and
311
+ then by ascending tag number, which matches the standard dictionaries for most
312
+ groups; pass `order` (e.g. `[448, 447, 452]`) when a group's spec order differs.
313
+
314
+ ### SessionID
315
+
316
+ ```ts
317
+ new SessionID(beginString: string, senderCompID: string, targetCompID: string, qualifier?: string);
318
+ sessionID.beginString; sessionID.senderCompID;
319
+ sessionID.targetCompID; sessionID.sessionQualifier;
320
+ sessionID.toString(); // e.g. "FIX.4.4:CLIENT->BROKER"
321
+ ```
322
+
323
+ ### SessionSettings
324
+
325
+ ```ts
326
+ SessionSettings.fromString(cfg: string): SessionSettings;
327
+ SessionSettings.fromFile(path: string): SessionSettings;
328
+ settings.getSessions(): SessionID[];
329
+ ```
330
+
331
+ ### DataDictionary
332
+
333
+ ```ts
334
+ DataDictionary.fromString(xml: string): DataDictionary;
335
+ DataDictionary.fromFile(xmlPath: string): DataDictionary;
336
+ dictionary.validate(msg: Message, bodyOnly?: boolean): void; // throws QuickFixError if invalid
337
+ dictionary.getVersion(): string; // e.g. 'FIX.4.4'
338
+ dictionary.getFieldName(tag: number): string | undefined;
339
+ dictionary.getFieldTag(name: string): number | undefined;
340
+ dictionary.isField(tag: number): boolean;
341
+ dictionary.isMsgType(msgType: string): boolean;
342
+ ```
343
+
344
+ `validate(msg, true)` validates as the *application* dictionary only: the
345
+ BeginString version check and header/trailer validation are skipped. A
346
+ dictionary also drives structural parsing — see `Message.parse` above.
347
+
348
+ ### Initiator / Acceptor (the session engine)
349
+
350
+ Both extend Node's `EventEmitter` and share the same options and lifecycle:
351
+
352
+ ```ts
353
+ new Initiator({ settings, handlers?, store?: 'file' | 'memory', log?: 'screen' | 'file' | 'none' });
354
+ new Acceptor({ settings, handlers?, store?, log? });
355
+
356
+ engine.start(): Promise<void>;
357
+ engine.stop(force?: boolean): Promise<void>; // graceful drain by default; force to abort
358
+ engine.isLoggedOn(sessionID?: SessionID): boolean; // any session, or just that one
359
+ engine.getSessions(): SessionID[]; // the [SESSION]s it was configured with
360
+ engine.getSession(sessionID: SessionID): Session | undefined;
361
+ engine.ref(): void; // keep the event loop alive while running (default)
362
+ engine.unref(): void; // let a short script / test exit
363
+ ```
364
+
365
+ `start`/`stop` resolve once the engine has finished starting up / shutting down;
366
+ they run off the main thread so your handlers can fire meanwhile. Once `stop()`
367
+ settles the engine's sessions are destroyed: `getSession` returns `undefined` and
368
+ any `Session` handle you still hold throws `SessionNotFound`. Until then they are
369
+ still live: a graceful `stop()` settles only after the `'logout'` events it
370
+ triggered have been delivered, so a `'logout'` listener can still read the
371
+ session, and a `sendToTarget()` that has already reached its session completes
372
+ before the sessions go away. One that has not reached it by then is refused
373
+ with `SessionNotFound` rather than run against the engine that is gone; a send
374
+ is bound to the engine that owns the session when it is called, never to one
375
+ created later with the same id.
376
+ `stop(true)` discards the events still queued and destroys the sessions right
377
+ away. A `stop()` called while another is still in flight (from that `'logout'`
378
+ listener, say) returns the same Promise, so it too settles only once the
379
+ sessions are gone; the first call's `force` applies to both.
380
+
381
+ Two live engines cannot share a `SessionID`: constructing one whose session
382
+ already exists in the process throws a `ConfigError` ("Duplicate Session") until
383
+ the engine owning it has been stopped. A constructor that fails on a later
384
+ `[SESSION]` (any `ConfigError`) cleans up the sessions it had already created
385
+ (so the ids are not locked out by the failed attempt) and the engine-level log
386
+ QuickFIX had already opened for it.
387
+
388
+ ### Session
389
+
390
+ A handle on one **live** session, obtained from `engine.getSession(id)` or the
391
+ module-level `lookupSession(id)`. It stores only the `SessionID` and the identity
392
+ of the engine it came from, and re-resolves the engine-owned `FIX::Session` on
393
+ every call, so it never dangles — once the session is gone (its engine was
394
+ stopped) every method throws a `QuickFixError` whose `fixErrorName` is
395
+ `'SessionNotFound'`, even after a new engine has been created with the same id.
396
+ A handle never crosses over to another engine's session; get a new one from the
397
+ new engine.
398
+
399
+ ```ts
400
+ session.sessionID: SessionID;
401
+
402
+ // State (sync)
403
+ session.isLoggedOn(): boolean; session.isEnabled(): boolean;
404
+ session.sentLogon(): boolean; session.sentLogout(): boolean; session.receivedLogon(): boolean;
405
+ session.isInitiator(): boolean; session.isAcceptor(): boolean;
406
+ session.isSessionTime(now?: Date): boolean; session.isLogonTime(now?: Date): boolean;
407
+
408
+ // Sequence numbers (sync; setters write to the message store -- on a quiet session, see below)
409
+ session.getExpectedSenderNum(): number; session.getExpectedTargetNum(): number;
410
+ session.setNextSenderMsgSeqNum(n: number): void; session.setNextTargetMsgSeqNum(n: number): void;
411
+
412
+ // Runtime options (sync; change the live session, not its SessionSettings)
413
+ session.getResetOnLogon() / setResetOnLogon(v: boolean) // likewise: ResetOnLogout,
414
+ session.getLogonTimeout() / setLogonTimeout(seconds: number) // ResetOnDisconnect, RefreshOnLogon,
415
+ session.getTimestampPrecision() / setTimestampPrecision(0..9) // CheckCompId, CheckLatency, MaxLatency,
416
+ // LogoutTimeout, PersistMessages,
417
+ // SendRedundantResendRequests,
418
+ // ValidateLengthAndChecksum,
419
+ // SendNextExpectedMsgSeqNum, IsNonStopSession
420
+
421
+ // Control
422
+ session.logon(): void; // enable; an initiator reconnects + logs on
423
+ session.logout(reason?: string): void; // disable + graceful Logout; stays down until logon()
424
+ session.refresh(): void; // re-read state from the message store
425
+ ```
426
+
427
+ Everything is synchronous: none of these members takes the mutex QuickFIX holds
428
+ while it runs your handlers, so they cannot deadlock the main thread — and
429
+ because they run inline, `session.logout('x'); session.logon();` takes effect in
430
+ that order. (`logon()`/`logout()` flip a plain flag the engine's network thread
431
+ reads without a lock, exactly as QuickFIX's own `Initiator::stop()` does from
432
+ the stopping thread; the engine picks the change up on its next pass.) The
433
+ sequence-number setters are likewise not serialised against a message the
434
+ engine is sending or receiving on that session at the same moment: QuickFIX
435
+ reads the number, runs your handler, then increments from the store under a
436
+ lock the setters do not take, so a value set in that window — from a handler,
437
+ or while a `sendToTarget()` or a heartbeat is in flight — is overwritten and the
438
+ session ends up one off from its counterparty. Renumber a **quiet** session:
439
+ before `start()`, from a `'logout'` event, or when you know nothing is in
440
+ flight. QuickFIX's `Session::disconnect()` and `reset()` are deliberately **not**
441
+ exposed: they drop the transport through the engine's socket monitor, which is
442
+ only safe from the engine's own network thread (QuickFIX itself never calls them
443
+ from anywhere else). Bring a session down with `logout()` and renumber it with
444
+ the sequence-number setters. Observe the outcome through the engine's
445
+ `'logon'` / `'logout'` events:
446
+
447
+ ```ts
448
+ initiator.on('logon', (id) => {
449
+ console.log('next outbound seq', initiator.getSession(id)!.getExpectedSenderNum());
450
+ });
451
+ initiator.on('logout', (id) => {
452
+ // The session is quiet now: renumber it, e.g. after the counterparty reset.
453
+ initiator.getSession(id)?.setNextTargetMsgSeqNum(1);
454
+ });
455
+
456
+ lookupSession(id)?.logout('maintenance'); // -> 'logout' event, isLoggedOn() === false
457
+ lookupSession(id)?.logon(); // -> reconnects, 'logon' event
458
+ ```
459
+
460
+ ### Handlers vs. events
461
+
462
+ This is the key distinction to understand:
463
+
464
+ - **`handlers`** (passed in the constructor options) run **synchronously** on the
465
+ engine's callback path. They are the *only* place you can affect a message:
466
+ - `toAdmin` / `toApp` may **mutate** the outbound message in place (e.g. add
467
+ credentials to a Logon in `toAdmin`).
468
+ - `toApp` may **throw** `new FixReject('DoNotSend')` to suppress sending.
469
+ - `fromAdmin` / `fromApp` may **throw** a `FixReject` to reject an inbound
470
+ message (e.g. `'RejectLogon'`, `'UnsupportedMessageType'`).
471
+ - `onCreate` / `onLogon` / `onLogout` are fire-and-forget notifications.
472
+
473
+ - **Events** (`engine.on('logon', ...)`, etc.) are **observe-only**. The engine
474
+ emits `'create' | 'logon' | 'logout' | 'toAdmin' | 'fromAdmin' | 'toApp' | 'fromApp'`
475
+ *after* the corresponding handler runs. Event listeners **cannot** mutate a
476
+ message or reject it — use `handlers` for that.
477
+
478
+ ### Rejections
479
+
480
+ Throw a `FixReject` from a handler to reject:
481
+
482
+ ```ts
483
+ import { FixReject, fixReject } from '@homaiohq/napi-quickfix';
484
+
485
+ throw new FixReject('DoNotSend');
486
+ throw new FixReject('RejectLogon', 'bad credentials');
487
+ throw fixReject('UnsupportedMessageType'); // convenience factory
488
+ ```
489
+
490
+ Recognized kinds: `'DoNotSend'`, `'RejectLogon'`, `'UnsupportedMessageType'`,
491
+ `'IncorrectDataFormat'`, `'IncorrectTagValue'`, `'FieldNotFound'`.
492
+
493
+ ### Module functions and constants
494
+
495
+ ```ts
496
+ sendToTarget(message: Message, sessionID: SessionID): Promise<boolean>;
497
+ sendToTarget(message: Message, qualifier?: string): Promise<boolean>; // session from the message header (8/49/56)
498
+
499
+ lookupSession(sessionID: SessionID): Session | undefined; // any engine in this process
500
+ doesSessionExist(sessionID: SessionID): boolean;
501
+ getSessions(): SessionID[]; // every session in this process, across all engines
502
+ numSessions(): number;
503
+
504
+ version(): string; // engine / addon version string
505
+
506
+ FIELD // field-name -> tag number, e.g. FIELD.MsgType === 35, FIELD.Password === 554
507
+ MsgType // MsgType values, e.g. MsgType.Logon === 'A'
508
+ Side // Side values, e.g. Side.Buy === '1'
509
+ OrdType // e.g. OrdType.Limit === '2'
510
+ TimeInForce // e.g. TimeInForce.Day === '0'
511
+ VALUES // every value group keyed by field name, e.g. VALUES.ExecType.Fill === '2'
512
+ enums // the full frozen tree: every value group plus FIELD
513
+ ```
514
+
515
+ `FIELD` is generated from the bundled QuickFIX's `FixFieldNumbers.h`, so it has every
516
+ `FIX::FIELD::*` name the engine knows (6000+). Values are literal types: `FIELD.Password`
517
+ is typed `554`, and a misspelt field name is a compile error. `FieldName` is the union of
518
+ all field names.
519
+
520
+ The value groups are generated the same way from `FixValues.h`: one frozen object per
521
+ field (690 groups, 5700+ values), each value a string because FIX is string-on-the-wire
522
+ (`EncryptMethod.None === '0'`, not `0`). Names derive from QuickFIX's
523
+ `FIX::<Field>_<VALUE>` constants with the SCREAMING_SNAKE suffix turned into PascalCase —
524
+ `Side_SELL_SHORT` → `Side.SellShort`, `ExecType_DONE_FOR_DAY` → `ExecType.DoneForDay` —
525
+ while `MsgType`, whose upstream names already mirror message names, is kept verbatim
526
+ (`MsgType.NewOrderSingle`, `MsgType.IOI`). Every word is lower-cased, acronyms included
527
+ (`SecurityIDSource_ISIN_NUMBER` → `SecurityIDSource.IsinNumber`, `MDEntryType_VWAP` →
528
+ `MDEntryType.Vwap`): the upstream names carry no acronym information, so there is no
529
+ allowlist to maintain and the keys are stable. The exact rule is documented in
530
+ `scripts/gen-values.mjs`. Values are literal types too (`Side.Buy` is typed `'1'`), and
531
+ `ValueGroupName` is the union of all group names.
532
+
533
+ The root entry exports `MsgType`, `Side`, `OrdType` and `TimeInForce` by name and every
534
+ group through `VALUES` / `enums`. Both tables are also exposed as subpath exports that
535
+ do **not** load the native addon, for tooling that only needs the constants (log
536
+ parsers, test helpers, bundles for platforms without a prebuild); each group is a named
537
+ export there, so bundlers resolving the ESM build keep only the groups you import (the
538
+ CJS build is not tree-shakeable: tsc drops the `@__PURE__` annotations when it emits
539
+ CommonJS):
540
+
541
+ ```ts
542
+ import { FIELD } from '@homaiohq/napi-quickfix/fields';
543
+ import { ExecType, OrdStatus, SecurityType } from '@homaiohq/napi-quickfix/values';
544
+ ```
545
+
546
+ ### Errors
547
+
548
+ Errors thrown by native calls are `Error`s with `name === 'QuickFixError'`,
549
+ carrying a `fixError` discriminator (a stable string such as `'ConfigError'`,
550
+ `'FieldNotFound'`, `'InvalidMessage'`, `'IncorrectTagValue'`) plus a `detail`
551
+ string, so you can branch on the failure kind:
552
+
553
+ ```ts
554
+ try {
555
+ dictionary.validate(msg);
556
+ } catch (err) {
557
+ if (err.name === 'QuickFixError' && err.fixError === 'FieldNotFound') {
558
+ // handle a missing required field
559
+ }
560
+ }
561
+ ```
562
+
563
+ ## Building from source / Contributing
564
+
565
+ Development uses **Yarn (Berry 4)** with the `node-modules` linker, via corepack.
566
+
567
+ ```sh
568
+ corepack enable
569
+ yarn install
570
+ yarn build # cmake-js native build + dual (ESM + CJS) TypeScript build
571
+ yarn test # node --test
572
+ ```
573
+
574
+ QuickFIX is fetched at build time via CMake `FetchContent` (pinned to
575
+ `v1.16.0`) — nothing is vendored or committed. The native build therefore needs
576
+ CMake (>= 3.12) and a C++17 compiler; the QuickFIX sources are downloaded
577
+ automatically on first configure.
578
+
579
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for more detail, and
580
+ [ARCHITECTURE.md](./ARCHITECTURE.md) for a diagrammed walkthrough of how the
581
+ TypeScript API, the C++ Node-API addon, and the QuickFIX engine fit together
582
+ (layers, build pipeline, the threading/async model, and CI).
583
+
584
+ ## Compatibility
585
+
586
+ | | |
587
+ | --- | --- |
588
+ | **Node.js** | LTS **22**, **24** and **26** (current) |
589
+ | **N-API version** | 9 |
590
+ | **Platforms** | Linux (glibc + musl/Alpine), macOS, Windows |
591
+ | **Prebuilt targets** | `linux-x64` (glibc, musl), `darwin-x64`, `darwin-arm64`, `win32-x64` |
592
+ | **libc (Linux)** | glibc **and** musl — separate libc-tagged prebuilds; musl is x64-only |
593
+ | **QuickFIX** | v1.16.0 (statically linked into prebuilds) |
594
+ | **Timestamp resolution** | microseconds on Linux (glibc + musl) and macOS; **milliseconds on Windows** |
595
+
596
+ Because prebuilds are N-API-tagged (not ABI-tagged), a single binary per
597
+ platform serves all supported Node LTS versions.
598
+
599
+ On Windows, QuickFIX has no microsecond clock available (it falls back to
600
+ `ftime()`), so sub-second `SendingTime` digits beyond milliseconds are always
601
+ zero — a `TimestampPrecision=6` session emits `.NNN000`. Setting a higher
602
+ precision is still valid and interoperable; only the extra digits are padding.
603
+
604
+ ## Versioning
605
+
606
+ This package follows [Semantic Versioning](https://semver.org/) and is currently
607
+ in the **`0.x`** (pre-1.0) phase. While in `0.x`, **minor releases may include
608
+ breaking changes** — pre-1.0 has no separate major channel — so review the
609
+ [CHANGELOG](./CHANGELOG.md) before upgrading across a minor bump. Note that npm's
610
+ caret range locks the minor for `0.x`: `^0.1.0` resolves to `>=0.1.0 <0.2.0`.
611
+
612
+ The package version tracks *this binding's* API and behavior, not the bundled
613
+ QuickFIX release (statically linked, currently `v1.16.0` — see the table above).
614
+ See [VERSIONING.md](./VERSIONING.md) for the full policy.
615
+
616
+ ## License
617
+
618
+ This package (`@homaiohq/napi-quickfix`) is licensed under the **MIT License** —
619
+ see [LICENSE](./LICENSE). Copyright (c) 2026 Homaio.
620
+
621
+ ### Third-party licenses
622
+
623
+ The published prebuilt binaries **statically link** the QuickFIX C++ engine,
624
+ which is distributed under the **QuickFIX Software License, Version 1.0** (a
625
+ permissive, BSD-style license). Its full text and the required attribution are
626
+ reproduced in [THIRD_PARTY_LICENSES](./THIRD_PARTY_LICENSES).
627
+
628
+ As required by that license:
629
+
630
+ > This product includes software developed by quickfixengine.org
631
+ > (http://www.quickfixengine.org/).
632
+
633
+ ### Acknowledgements / Naming
634
+
635
+ This is an **independent, unaffiliated** Node.js binding. It is not produced,
636
+ sponsored, or endorsed by quickfixengine.org or the QuickFIX project. The name
637
+ "QuickFIX" is used here **nominatively** — solely to describe the C++ library
638
+ that this package wraps — and does not imply any affiliation or endorsement. All
639
+ QuickFIX trademarks and names remain the property of their respective owners.