@homaiohq/napi-quickfix 0.1.0 → 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 (141) hide show
  1. package/CMakeLists.txt +5 -1
  2. package/README.md +235 -18
  3. package/cpp/acceptor_wrap.cpp +253 -19
  4. package/cpp/acceptor_wrap.h +60 -1
  5. package/cpp/addon.cpp +10 -6
  6. package/cpp/application_bridge.cpp +114 -30
  7. package/cpp/application_bridge.h +57 -4
  8. package/cpp/data_dictionary_wrap.cpp +89 -3
  9. package/cpp/data_dictionary_wrap.h +27 -5
  10. package/cpp/engine_common.h +108 -0
  11. package/cpp/engine_workers.h +99 -18
  12. package/cpp/field_map_ops.h +492 -0
  13. package/cpp/group_wrap.cpp +221 -0
  14. package/cpp/group_wrap.h +76 -0
  15. package/cpp/initiator_wrap.cpp +262 -25
  16. package/cpp/initiator_wrap.h +37 -2
  17. package/cpp/message_wrap.cpp +171 -78
  18. package/cpp/message_wrap.h +47 -5
  19. package/cpp/session_op_gate.h +201 -0
  20. package/cpp/session_static.cpp +139 -12
  21. package/cpp/session_wrap.cpp +438 -0
  22. package/cpp/session_wrap.h +144 -0
  23. package/dist/cjs/acceptor.d.ts +0 -1
  24. package/dist/cjs/acceptor.js +0 -1
  25. package/dist/cjs/application.d.ts +0 -1
  26. package/dist/cjs/application.js +0 -1
  27. package/dist/cjs/data-dictionary.d.ts +21 -3
  28. package/dist/cjs/data-dictionary.js +32 -4
  29. package/dist/cjs/engine.d.ts +42 -4
  30. package/dist/cjs/engine.js +50 -5
  31. package/dist/cjs/enums.d.ts +48 -14
  32. package/dist/cjs/enums.js +20 -25
  33. package/dist/cjs/field-value.d.ts +12 -0
  34. package/dist/cjs/field-value.js +40 -0
  35. package/dist/cjs/generated/fields.d.ts +0 -1
  36. package/dist/cjs/generated/fields.js +0 -1
  37. package/dist/cjs/generated/values.d.ts +8548 -0
  38. package/dist/cjs/generated/values.js +8580 -0
  39. package/dist/cjs/group.d.ts +126 -0
  40. package/dist/cjs/group.js +185 -0
  41. package/dist/cjs/index.d.ts +26 -5
  42. package/dist/cjs/index.js +28 -28
  43. package/dist/cjs/initiator.d.ts +0 -1
  44. package/dist/cjs/initiator.js +0 -1
  45. package/dist/cjs/message.d.ts +129 -5
  46. package/dist/cjs/message.js +146 -9
  47. package/dist/cjs/native.d.ts +179 -17
  48. package/dist/cjs/native.js +0 -1
  49. package/dist/cjs/session-id.d.ts +12 -1
  50. package/dist/cjs/session-id.js +21 -1
  51. package/dist/cjs/session-settings.d.ts +0 -1
  52. package/dist/cjs/session-settings.js +0 -1
  53. package/dist/cjs/session.d.ts +213 -0
  54. package/dist/cjs/session.js +337 -0
  55. package/dist/esm/acceptor.d.ts +0 -1
  56. package/dist/esm/acceptor.js +0 -1
  57. package/dist/esm/application.d.ts +0 -1
  58. package/dist/esm/application.js +0 -1
  59. package/dist/esm/data-dictionary.d.ts +21 -3
  60. package/dist/esm/data-dictionary.js +32 -4
  61. package/dist/esm/engine.d.ts +42 -4
  62. package/dist/esm/engine.js +51 -6
  63. package/dist/esm/enums.d.ts +48 -14
  64. package/dist/esm/enums.js +14 -24
  65. package/dist/esm/field-value.d.ts +12 -0
  66. package/dist/esm/field-value.js +37 -0
  67. package/dist/esm/generated/fields.d.ts +0 -1
  68. package/dist/esm/generated/fields.js +0 -1
  69. package/dist/esm/generated/values.d.ts +8548 -0
  70. package/dist/esm/generated/values.js +8564 -0
  71. package/dist/esm/group.d.ts +126 -0
  72. package/dist/esm/group.js +181 -0
  73. package/dist/esm/index.d.ts +26 -5
  74. package/dist/esm/index.js +17 -26
  75. package/dist/esm/initiator.d.ts +0 -1
  76. package/dist/esm/initiator.js +0 -1
  77. package/dist/esm/message.d.ts +129 -5
  78. package/dist/esm/message.js +146 -9
  79. package/dist/esm/native.d.ts +179 -17
  80. package/dist/esm/native.js +0 -1
  81. package/dist/esm/session-id.d.ts +12 -1
  82. package/dist/esm/session-id.js +20 -1
  83. package/dist/esm/session-settings.d.ts +0 -1
  84. package/dist/esm/session-settings.js +0 -1
  85. package/dist/esm/session.d.ts +213 -0
  86. package/dist/esm/session.js +329 -0
  87. package/package.json +13 -1
  88. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  89. package/prebuilds/darwin-x64/node.napi.node +0 -0
  90. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  91. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  92. package/prebuilds/win32-x64/node.napi.node +0 -0
  93. package/cpp/enums.cpp +0 -66
  94. package/dist/cjs/acceptor.d.ts.map +0 -1
  95. package/dist/cjs/acceptor.js.map +0 -1
  96. package/dist/cjs/application.d.ts.map +0 -1
  97. package/dist/cjs/application.js.map +0 -1
  98. package/dist/cjs/data-dictionary.d.ts.map +0 -1
  99. package/dist/cjs/data-dictionary.js.map +0 -1
  100. package/dist/cjs/engine.d.ts.map +0 -1
  101. package/dist/cjs/engine.js.map +0 -1
  102. package/dist/cjs/enums.d.ts.map +0 -1
  103. package/dist/cjs/enums.js.map +0 -1
  104. package/dist/cjs/generated/fields.d.ts.map +0 -1
  105. package/dist/cjs/generated/fields.js.map +0 -1
  106. package/dist/cjs/index.d.ts.map +0 -1
  107. package/dist/cjs/index.js.map +0 -1
  108. package/dist/cjs/initiator.d.ts.map +0 -1
  109. package/dist/cjs/initiator.js.map +0 -1
  110. package/dist/cjs/message.d.ts.map +0 -1
  111. package/dist/cjs/message.js.map +0 -1
  112. package/dist/cjs/native.d.ts.map +0 -1
  113. package/dist/cjs/native.js.map +0 -1
  114. package/dist/cjs/session-id.d.ts.map +0 -1
  115. package/dist/cjs/session-id.js.map +0 -1
  116. package/dist/cjs/session-settings.d.ts.map +0 -1
  117. package/dist/cjs/session-settings.js.map +0 -1
  118. package/dist/esm/acceptor.d.ts.map +0 -1
  119. package/dist/esm/acceptor.js.map +0 -1
  120. package/dist/esm/application.d.ts.map +0 -1
  121. package/dist/esm/application.js.map +0 -1
  122. package/dist/esm/data-dictionary.d.ts.map +0 -1
  123. package/dist/esm/data-dictionary.js.map +0 -1
  124. package/dist/esm/engine.d.ts.map +0 -1
  125. package/dist/esm/engine.js.map +0 -1
  126. package/dist/esm/enums.d.ts.map +0 -1
  127. package/dist/esm/enums.js.map +0 -1
  128. package/dist/esm/generated/fields.d.ts.map +0 -1
  129. package/dist/esm/generated/fields.js.map +0 -1
  130. package/dist/esm/index.d.ts.map +0 -1
  131. package/dist/esm/index.js.map +0 -1
  132. package/dist/esm/initiator.d.ts.map +0 -1
  133. package/dist/esm/initiator.js.map +0 -1
  134. package/dist/esm/message.d.ts.map +0 -1
  135. package/dist/esm/message.js.map +0 -1
  136. package/dist/esm/native.d.ts.map +0 -1
  137. package/dist/esm/native.js.map +0 -1
  138. package/dist/esm/session-id.d.ts.map +0 -1
  139. package/dist/esm/session-id.js.map +0 -1
  140. package/dist/esm/session-settings.d.ts.map +0 -1
  141. package/dist/esm/session-settings.js.map +0 -1
package/CMakeLists.txt CHANGED
@@ -85,7 +85,11 @@ string(REPLACE "\n" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
85
85
  # ---------------------------------------------------------------------------
86
86
  # The native addon.
87
87
  # ---------------------------------------------------------------------------
88
- file(GLOB ADDON_SOURCES "${CMAKE_SOURCE_DIR}/cpp/*.cpp")
88
+ # CONFIGURE_DEPENDS re-globs on every build, so adding or deleting a cpp/*.cpp
89
+ # file is picked up by an already-configured tree. cmake-js only re-runs the
90
+ # configure step when CMakeCache.txt is missing, so without it a deleted source
91
+ # stays in the build graph until `yarn clean`.
92
+ file(GLOB ADDON_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/cpp/*.cpp")
89
93
 
90
94
  add_library(napi_quickfix SHARED ${ADDON_SOURCES} ${CMAKE_JS_SRC})
91
95
  set_target_properties(napi_quickfix PROPERTIES PREFIX "" SUFFIX ".node")
package/README.md CHANGED
@@ -23,11 +23,17 @@ included.
23
23
  points.
24
24
  - **TypeScript types included** — hand-written `.d.ts` for the full public API.
25
25
  - **Full FIX surface** — the session engine (`Initiator`/`Acceptor` +
26
- synchronous application handlers) *and* the pure layer (`Message`,
27
- `SessionSettings`, `DataDictionary`, `SessionID`).
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`).
28
30
  - **Ergonomic, safe API** — chainable message building with automatic
29
31
  header/trailer field routing, typed rejections, and C++ exceptions surfaced as
30
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.
31
37
 
32
38
  ## Install
33
39
 
@@ -206,20 +212,41 @@ come from `@homaiohq/napi-quickfix`.
206
212
  ### Message
207
213
 
208
214
  ```ts
209
- new Message(raw?: string, opts?: { validate?: boolean });
210
- Message.parse(raw: string, opts?: { validate?: boolean }): Message;
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 }
211
219
 
212
220
  message.setField(tag: number, value: string | number): this; // chainable, auto-routes header/trailer fields
213
- message.getField(tag: number): string;
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;
214
228
  message.setHeaderField(tag, value): this; message.getHeaderField(tag): string;
215
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
+
216
243
  message.getMsgType(): string;
217
244
  message.toString(): string; // raw SOH-delimited wire string
218
245
  message.toPretty(): string; // human-readable
219
246
  message.toJSON(): { msgType?: string; raw: string };
220
247
 
221
248
  createMessage(fields?: Record<number, string | number>): Message;
222
- parseMessage(raw: string, opts?: { validate?: boolean }): Message;
249
+ parseMessage(raw: string, opts?: MessageParseOptions): Message;
223
250
  ```
224
251
 
225
252
  Field values are accepted as `string | number` (numbers are stringified) and
@@ -228,6 +255,62 @@ always returned as `string`. Header/trailer fields such as `MsgType`,
228
255
  section by `setField`, so you rarely need `setHeaderField`/`setTrailerField`
229
256
  directly.
230
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
+
231
314
  ### SessionID
232
315
 
233
316
  ```ts
@@ -250,9 +333,18 @@ settings.getSessions(): SessionID[];
250
333
  ```ts
251
334
  DataDictionary.fromString(xml: string): DataDictionary;
252
335
  DataDictionary.fromFile(xmlPath: string): DataDictionary;
253
- dictionary.validate(msg: Message): void; // throws QuickFixError if invalid
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;
254
342
  ```
255
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
+
256
348
  ### Initiator / Acceptor (the session engine)
257
349
 
258
350
  Both extend Node's `EventEmitter` and share the same options and lifecycle:
@@ -261,13 +353,110 @@ Both extend Node's `EventEmitter` and share the same options and lifecycle:
261
353
  new Initiator({ settings, handlers?, store?: 'file' | 'memory', log?: 'screen' | 'file' | 'none' });
262
354
  new Acceptor({ settings, handlers?, store?, log? });
263
355
 
264
- engine.start(): void;
265
- engine.stop(force?: boolean): void; // graceful drain by default; force to abort
266
- engine.isLoggedOn(): boolean;
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;
267
361
  engine.ref(): void; // keep the event loop alive while running (default)
268
362
  engine.unref(): void; // let a short script / test exit
269
363
  ```
270
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
+
271
460
  ### Handlers vs. events
272
461
 
273
462
  This is the key distinction to understand:
@@ -304,13 +493,23 @@ Recognized kinds: `'DoNotSend'`, `'RejectLogon'`, `'UnsupportedMessageType'`,
304
493
  ### Module functions and constants
305
494
 
306
495
  ```ts
307
- sendToTarget(message: Message, sessionID: SessionID): boolean;
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
+
308
504
  version(): string; // engine / addon version string
309
505
 
310
- FIELD // field-name -> tag number, e.g. FIELD.MsgType === 35, FIELD.Password === 554
311
- MsgType // MsgType values, e.g. MsgType.Logon === 'A'
312
- Side // Side values, e.g. Side.Buy === '1'
313
- enums // the full frozen tree of FIX constants
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
314
513
  ```
315
514
 
316
515
  `FIELD` is generated from the bundled QuickFIX's `FixFieldNumbers.h`, so it has every
@@ -318,12 +517,30 @@ enums // the full frozen tree of FIX constants
318
517
  is typed `554`, and a misspelt field name is a compile error. `FieldName` is the union of
319
518
  all field names.
320
519
 
321
- The table is also exposed as a subpath export that does **not** load the native addon,
322
- for tooling that only needs tag numbers (log parsers, test helpers, bundles for platforms
323
- without a prebuild):
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):
324
540
 
325
541
  ```ts
326
542
  import { FIELD } from '@homaiohq/napi-quickfix/fields';
543
+ import { ExecType, OrdStatus, SecurityType } from '@homaiohq/napi-quickfix/values';
327
544
  ```
328
545
 
329
546
  ### Errors