@ignex/nova 0.1.1

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +313 -0
  3. package/docs/architecture.md +146 -0
  4. package/docs/publishing.md +119 -0
  5. package/docs/wire-format.md +170 -0
  6. package/index.ts +61 -0
  7. package/package.json +89 -0
  8. package/prebuilds/linux-x64/libignex_ffi.so +0 -0
  9. package/public/client.ts +23 -0
  10. package/public/nats.ts +19 -0
  11. package/public/server.ts +35 -0
  12. package/rust/Cargo.toml +19 -0
  13. package/rust/src/ffi.rs +135 -0
  14. package/rust/src/generated/backend.rs +2817 -0
  15. package/rust/src/generated/mod.rs +2 -0
  16. package/rust/src/lib.rs +9 -0
  17. package/rust/src/transcode/generated.rs +1352 -0
  18. package/rust/src/transcode/mod.rs +2 -0
  19. package/src/bridge/nats.ts +269 -0
  20. package/src/bridge/subjects.ts +30 -0
  21. package/src/core/auth.ts +56 -0
  22. package/src/core/backpressure.ts +39 -0
  23. package/src/core/client-heartbeat.ts +27 -0
  24. package/src/core/client-reconnect.ts +35 -0
  25. package/src/core/client-state.ts +76 -0
  26. package/src/core/client-wire.ts +72 -0
  27. package/src/core/client.ts +176 -0
  28. package/src/core/groups.ts +52 -0
  29. package/src/core/int64-guard.ts +44 -0
  30. package/src/core/metrics.ts +105 -0
  31. package/src/core/outbound.ts +76 -0
  32. package/src/core/replay.ts +31 -0
  33. package/src/core/ring.ts +85 -0
  34. package/src/core/rooms.ts +44 -0
  35. package/src/core/routing.ts +94 -0
  36. package/src/core/server.ts +294 -0
  37. package/src/core/state.ts +179 -0
  38. package/src/generated/direct-ser.ts +495 -0
  39. package/src/generated/fbs/backend.fbs +139 -0
  40. package/src/generated/registry.ts +341 -0
  41. package/src/generated/rust/backend_generated.rs +2817 -0
  42. package/src/generated/ts/backend.ts +25 -0
  43. package/src/generated/ts/big-val.ts +106 -0
  44. package/src/generated/ts/complex.ts +303 -0
  45. package/src/generated/ts/customer.ts +137 -0
  46. package/src/generated/ts/hello.ts +123 -0
  47. package/src/generated/ts/join-group.ts +78 -0
  48. package/src/generated/ts/leave-group.ts +78 -0
  49. package/src/generated/ts/order-billing.ts +137 -0
  50. package/src/generated/ts/order-line.ts +144 -0
  51. package/src/generated/ts/order.ts +236 -0
  52. package/src/generated/ts/ping.ts +74 -0
  53. package/src/generated/ts/pong.ts +74 -0
  54. package/src/generated/ts/portfolio-position.ts +120 -0
  55. package/src/generated/ts/portfolio-snapshot.ts +170 -0
  56. package/src/generated/ts/quote.ts +148 -0
  57. package/src/generated/ts/side.ts +8 -0
  58. package/src/generated/ts/snapshot-request.ts +78 -0
  59. package/src/generated/ts/subscribe.ts +78 -0
  60. package/src/generated/ts/tags.ts +9 -0
  61. package/src/generated/ts/trade.ts +135 -0
  62. package/src/generated/ts/unsubscribe.ts +78 -0
  63. package/src/generated/ts/welcome.ts +112 -0
  64. package/src/generated/ts-ser.ts +465 -0
  65. package/src/generated/wire-registry.json +20 -0
  66. package/src/native/codec.ts +35 -0
  67. package/src/native/ffi.ts +214 -0
  68. package/src/native/loader.ts +55 -0
  69. package/src/schema/index.ts +217 -0
  70. package/src/server.ts +87 -0
  71. package/src/transport/byte-buffer-pool.ts +63 -0
  72. package/src/transport/scratch.ts +48 -0
  73. package/src/transport/stats.ts +44 -0
  74. package/src/transport/transport.ts +106 -0
@@ -0,0 +1,495 @@
1
+ // @generated by scripts/direct-gen.ts — DO NOT EDIT
2
+ import { encodeUtf8Into, ensureCapacity, utf8Len } from "../native/codec";
3
+ import { checkInt64 } from "../core/int64-guard";
4
+ import { anyEventNameToId, WIRE_VERSION, WIRE_HEADER_LEN } from "./registry";
5
+ import type { AnyEventName, ControlEvents, Events } from "../schema";
6
+
7
+ export type DirectCall = (...args: unknown[]) => number;
8
+
9
+ /** dlopen specs for the direct fast-path symbols (canonical form — the
10
+ * output tail `(ptr, usize)` is upgraded to `(buffer, buffer_length)` by
11
+ * the `abi()` transformer in `src/native/ffi.ts` when supported). */
12
+ export const directSymbols = {
13
+ fb_quote_serialize: {
14
+ args: ["cstring", "f64", "f64", "i64", "i64", "i64", "ptr", "usize"],
15
+ returns: "u64_fast",
16
+ },
17
+ fb_trade_serialize: {
18
+ args: ["cstring", "f64", "i64", "u32", "i64", "ptr", "usize"],
19
+ returns: "u64_fast",
20
+ },
21
+ fb_portfolio_serialize: {
22
+ args: ["cstring", "buffer", "usize", "f64", "f64", "i64", "cstring", "ptr", "usize"],
23
+ returns: "u64_fast",
24
+ },
25
+ fb_complex_serialize: {
26
+ args: ["cstring", "buffer", "usize", "buffer", "usize", "buffer", "usize", "buffer", "usize", "buffer", "usize", "u8", "f64", "i64", "ptr", "usize"],
27
+ returns: "u64_fast",
28
+ },
29
+ fb_big_val_serialize: {
30
+ args: ["cstring", "i64", "i64", "ptr", "usize"],
31
+ returns: "u64_fast",
32
+ },
33
+ fb_hello_serialize: {
34
+ args: ["i64", "buffer", "usize", "i64", "ptr", "usize"],
35
+ returns: "u64_fast",
36
+ },
37
+ fb_welcome_serialize: {
38
+ args: ["cstring", "buffer", "usize", "ptr", "usize"],
39
+ returns: "u64_fast",
40
+ },
41
+ fb_subscribe_serialize: {
42
+ args: ["cstring", "ptr", "usize"],
43
+ returns: "u64_fast",
44
+ },
45
+ fb_unsubscribe_serialize: {
46
+ args: ["cstring", "ptr", "usize"],
47
+ returns: "u64_fast",
48
+ },
49
+ fb_join_group_serialize: {
50
+ args: ["cstring", "ptr", "usize"],
51
+ returns: "u64_fast",
52
+ },
53
+ fb_leave_group_serialize: {
54
+ args: ["cstring", "ptr", "usize"],
55
+ returns: "u64_fast",
56
+ },
57
+ fb_snapshot_request_serialize: {
58
+ args: ["cstring", "ptr", "usize"],
59
+ returns: "u64_fast",
60
+ },
61
+ fb_ping_serialize: {
62
+ args: ["i64", "ptr", "usize"],
63
+ returns: "u64_fast",
64
+ },
65
+ fb_pong_serialize: {
66
+ args: ["i64", "ptr", "usize"],
67
+ returns: "u64_fast",
68
+ },
69
+ } as const;
70
+
71
+ /** sample objects for the bind-time self-test (representative values). */
72
+ const directSamples: Record<string, unknown> = {
73
+ "quote": { "symbol": "SELFTEST", "bid": 1.5, "ask": 1.5, "bidSize": 123456, "askSize": 123456, "ts": 123456 },
74
+ "trade": { "symbol": "SELFTEST", "price": 1.5, "volume": 123456, "side": "buy", "ts": 123456 },
75
+ "portfolio": { "accountId": "SELFTEST", "positions": [{ "symbol": "SELFTEST", "quantity": 123456, "avgPrice": 1.5, "pnl": 1.5 }], "totalValue": 1.5, "cash": 1.5, "ts": 123456, "updatedBy": "SELFTEST" },
76
+ "complex": { "id": "SELFTEST", "names": ["a", "b"], "prices": [1.5, 2.5], "counts": [10, 20], "flags": [true, false], "tags": ["hot"], "active": true, "total": 1.5, "ts": 123456 },
77
+ "bigVal": { "id": "SELFTEST", "seq": 123456, "when": 123456 },
78
+ "hello": { "version": 123456, "caps": ["a", "b"], "lastSeq": 123456 },
79
+ "welcome": { "clientId": "SELFTEST", "groups": ["a", "b"] },
80
+ "subscribe": { "topic": "SELFTEST" },
81
+ "unsubscribe": { "topic": "SELFTEST" },
82
+ "joinGroup": { "group": "SELFTEST" },
83
+ "leaveGroup": { "group": "SELFTEST" },
84
+ "snapshotRequest": { "topic": "SELFTEST" },
85
+ "ping": { "ts": 123456 },
86
+ "pong": { "ts": 123456 },
87
+ } as const;
88
+
89
+ /** grow the packer scratch, write the u32 count, return the DataView. */
90
+ function packHeader(holder: { v: Uint8Array }, dv: { v: DataView }, total: number, count: number): DataView {
91
+ if (holder.v.byteLength < total) { holder.v = ensureCapacity(holder.v, total); dv.v = new DataView(holder.v.buffer); }
92
+ const b = dv.v;
93
+ b.setUint32(0, count, true);
94
+ return b;
95
+ }
96
+
97
+ const SIDE = { buy: 0, sell: 1 } as const;
98
+ const TAGS = { hot: 0, new: 1, sale: 2 } as const;
99
+
100
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
101
+ export function encodeQuote(call: DirectCall, o: unknown, out: Uint8Array): number {
102
+ const p = o as Events["quote"];
103
+ checkInt64("quote.bidSize", p.bidSize);
104
+ checkInt64("quote.askSize", p.askSize);
105
+ checkInt64("quote.ts", p.ts);
106
+ return call(p.symbol, p.bid, p.ask, p.bidSize, p.askSize, p.ts, out, out) as number;
107
+ }
108
+
109
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
110
+ export function encodeTrade(call: DirectCall, o: unknown, out: Uint8Array): number {
111
+ const p = o as Events["trade"];
112
+ checkInt64("trade.volume", p.volume);
113
+ checkInt64("trade.ts", p.ts);
114
+ return call(p.symbol, p.price, p.volume, SIDE[p.side], p.ts, out, out) as number;
115
+ }
116
+
117
+ const portfolio_positionsHolder = { v: new Uint8Array(1024) };
118
+ const portfolio_positionsDv = { v: new DataView(portfolio_positionsHolder.v.buffer) };
119
+ function packPortfolioPositions(items: Events["portfolio"]["positions"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
120
+ let total = 4;
121
+ for (const p of items) total += 4 + utf8Len(p.symbol) + 8 + 8 + 8;
122
+ const b = packHeader(holder, dv, total, items.length);
123
+ let off = 4;
124
+ for (const p of items) {
125
+ const symbolL = encodeUtf8Into(p.symbol, holder.v, off + 4); b.setUint32(off, symbolL, true); off += 4; off += symbolL;
126
+ b.setInt32(off, p.quantity | 0, true); b.setInt32(off + 4, Math.floor(p.quantity / 0x100000000), true); off += 8;
127
+ b.setFloat64(off, p.avgPrice, true); off += 8;
128
+ b.setFloat64(off, p.pnl, true); off += 8;
129
+ }
130
+ return total;
131
+ }
132
+
133
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
134
+ export function encodePortfolio(call: DirectCall, o: unknown, out: Uint8Array): number {
135
+ const p = o as Events["portfolio"];
136
+ const positionsLen = packPortfolioPositions(p.positions, portfolio_positionsHolder, portfolio_positionsDv);
137
+ checkInt64("portfolio.ts", p.ts);
138
+ return call(p.accountId, portfolio_positionsHolder.v, positionsLen, p.totalValue, p.cash, p.ts, p.updatedBy ?? null, out, out) as number;
139
+ }
140
+
141
+ const complex_namesHolder = { v: new Uint8Array(1024) };
142
+ const complex_namesDv = { v: new DataView(complex_namesHolder.v.buffer) };
143
+ const complex_pricesHolder = { v: new Uint8Array(1024) };
144
+ const complex_pricesDv = { v: new DataView(complex_pricesHolder.v.buffer) };
145
+ const complex_countsHolder = { v: new Uint8Array(1024) };
146
+ const complex_countsDv = { v: new DataView(complex_countsHolder.v.buffer) };
147
+ const complex_flagsHolder = { v: new Uint8Array(1024) };
148
+ const complex_flagsDv = { v: new DataView(complex_flagsHolder.v.buffer) };
149
+ const complex_tagsHolder = { v: new Uint8Array(1024) };
150
+ const complex_tagsDv = { v: new DataView(complex_tagsHolder.v.buffer) };
151
+ function packComplexNames(items: Events["complex"]["names"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
152
+ let total = 4;
153
+ for (const x of items) total += 4 + utf8Len(x);
154
+ const b = packHeader(holder, dv, total, items.length);
155
+ let off = 4;
156
+ for (const x of items) {
157
+ const l = encodeUtf8Into(x, holder.v, off + 4); b.setUint32(off, l, true); off += 4; off += l;
158
+ }
159
+ return total;
160
+ }
161
+
162
+ function packComplexPrices(items: Events["complex"]["prices"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
163
+ let total = 4;
164
+ for (const x of items) total += 8;
165
+ const b = packHeader(holder, dv, total, items.length);
166
+ let off = 4;
167
+ for (const x of items) {
168
+ b.setFloat64(off, x, true); off += 8;
169
+ }
170
+ return total;
171
+ }
172
+
173
+ function packComplexCounts(items: Events["complex"]["counts"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
174
+ let total = 4;
175
+ for (const x of items) total += 8;
176
+ const b = packHeader(holder, dv, total, items.length);
177
+ let off = 4;
178
+ for (const x of items) {
179
+ b.setInt32(off, x | 0, true); b.setInt32(off + 4, Math.floor(x / 0x100000000), true); off += 8;
180
+ }
181
+ return total;
182
+ }
183
+
184
+ function packComplexFlags(items: Events["complex"]["flags"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
185
+ let total = 4;
186
+ for (const x of items) total += 1;
187
+ const b = packHeader(holder, dv, total, items.length);
188
+ let off = 4;
189
+ for (const x of items) {
190
+ b.setUint8(off, x ? 1 : 0); off += 1;
191
+ }
192
+ return total;
193
+ }
194
+
195
+ function packComplexTags(items: Events["complex"]["tags"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
196
+ let total = 4;
197
+ for (const x of items) total += 4;
198
+ const b = packHeader(holder, dv, total, items.length);
199
+ let off = 4;
200
+ for (const x of items) {
201
+ b.setUint32(off, TAGS[x], true); off += 4;
202
+ }
203
+ return total;
204
+ }
205
+
206
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
207
+ export function encodeComplex(call: DirectCall, o: unknown, out: Uint8Array): number {
208
+ const p = o as Events["complex"];
209
+ const namesLen = packComplexNames(p.names, complex_namesHolder, complex_namesDv);
210
+ const pricesLen = packComplexPrices(p.prices, complex_pricesHolder, complex_pricesDv);
211
+ const countsLen = packComplexCounts(p.counts, complex_countsHolder, complex_countsDv);
212
+ const flagsLen = packComplexFlags(p.flags, complex_flagsHolder, complex_flagsDv);
213
+ const tagsLen = packComplexTags(p.tags, complex_tagsHolder, complex_tagsDv);
214
+ checkInt64("complex.ts", p.ts);
215
+ return call(p.id, complex_namesHolder.v, namesLen, complex_pricesHolder.v, pricesLen, complex_countsHolder.v, countsLen, complex_flagsHolder.v, flagsLen, complex_tagsHolder.v, tagsLen, p.active ? 1 : 0, p.total, p.ts, out, out) as number;
216
+ }
217
+
218
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
219
+ export function encodeBigVal(call: DirectCall, o: unknown, out: Uint8Array): number {
220
+ const p = o as Events["bigVal"];
221
+ checkInt64("bigVal.when", p.when);
222
+ return call(p.id, p.seq, p.when, out, out) as number;
223
+ }
224
+
225
+ const hello_capsHolder = { v: new Uint8Array(1024) };
226
+ const hello_capsDv = { v: new DataView(hello_capsHolder.v.buffer) };
227
+ function packHelloCaps(items: ControlEvents["hello"]["caps"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
228
+ let total = 4;
229
+ for (const x of items) total += 4 + utf8Len(x);
230
+ const b = packHeader(holder, dv, total, items.length);
231
+ let off = 4;
232
+ for (const x of items) {
233
+ const l = encodeUtf8Into(x, holder.v, off + 4); b.setUint32(off, l, true); off += 4; off += l;
234
+ }
235
+ return total;
236
+ }
237
+
238
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
239
+ export function encodeHello(call: DirectCall, o: unknown, out: Uint8Array): number {
240
+ const p = o as ControlEvents["hello"];
241
+ checkInt64("hello.version", p.version);
242
+ const capsLen = packHelloCaps(p.caps, hello_capsHolder, hello_capsDv);
243
+ checkInt64("hello.lastSeq", p.lastSeq);
244
+ return call(p.version, hello_capsHolder.v, capsLen, p.lastSeq, out, out) as number;
245
+ }
246
+
247
+ const welcome_groupsHolder = { v: new Uint8Array(1024) };
248
+ const welcome_groupsDv = { v: new DataView(welcome_groupsHolder.v.buffer) };
249
+ function packWelcomeGroups(items: ControlEvents["welcome"]["groups"], holder: { v: Uint8Array }, dv: { v: DataView }): number {
250
+ let total = 4;
251
+ for (const x of items) total += 4 + utf8Len(x);
252
+ const b = packHeader(holder, dv, total, items.length);
253
+ let off = 4;
254
+ for (const x of items) {
255
+ const l = encodeUtf8Into(x, holder.v, off + 4); b.setUint32(off, l, true); off += 4; off += l;
256
+ }
257
+ return total;
258
+ }
259
+
260
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
261
+ export function encodeWelcome(call: DirectCall, o: unknown, out: Uint8Array): number {
262
+ const p = o as ControlEvents["welcome"];
263
+ const groupsLen = packWelcomeGroups(p.groups, welcome_groupsHolder, welcome_groupsDv);
264
+ return call(p.clientId, welcome_groupsHolder.v, groupsLen, out, out) as number;
265
+ }
266
+
267
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
268
+ export function encodeSubscribe(call: DirectCall, o: unknown, out: Uint8Array): number {
269
+ const p = o as ControlEvents["subscribe"];
270
+ return call(p.topic, out, out) as number;
271
+ }
272
+
273
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
274
+ export function encodeUnsubscribe(call: DirectCall, o: unknown, out: Uint8Array): number {
275
+ const p = o as ControlEvents["unsubscribe"];
276
+ return call(p.topic, out, out) as number;
277
+ }
278
+
279
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
280
+ export function encodeJoinGroup(call: DirectCall, o: unknown, out: Uint8Array): number {
281
+ const p = o as ControlEvents["joinGroup"];
282
+ return call(p.group, out, out) as number;
283
+ }
284
+
285
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
286
+ export function encodeLeaveGroup(call: DirectCall, o: unknown, out: Uint8Array): number {
287
+ const p = o as ControlEvents["leaveGroup"];
288
+ return call(p.group, out, out) as number;
289
+ }
290
+
291
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
292
+ export function encodeSnapshotRequest(call: DirectCall, o: unknown, out: Uint8Array): number {
293
+ const p = o as ControlEvents["snapshotRequest"];
294
+ return call(p.topic, out, out) as number;
295
+ }
296
+
297
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
298
+ export function encodePing(call: DirectCall, o: unknown, out: Uint8Array): number {
299
+ const p = o as ControlEvents["ping"];
300
+ checkInt64("ping.ts", p.ts);
301
+ return call(p.ts, out, out) as number;
302
+ }
303
+
304
+ /** zero-alloc encoder: fields straight into the (reusable) out buffer. */
305
+ export function encodePong(call: DirectCall, o: unknown, out: Uint8Array): number {
306
+ const p = o as ControlEvents["pong"];
307
+ checkInt64("pong.ts", p.ts);
308
+ return call(p.ts, out, out) as number;
309
+ }
310
+
311
+ function hasNulQuote(o: unknown): boolean {
312
+ const p = o as Events["quote"];
313
+ return (
314
+ (typeof p.symbol === "string" && p.symbol.includes("\0"))
315
+ );
316
+ }
317
+
318
+ function hasNulTrade(o: unknown): boolean {
319
+ const p = o as Events["trade"];
320
+ return (
321
+ (typeof p.symbol === "string" && p.symbol.includes("\0"))
322
+ );
323
+ }
324
+
325
+ function hasNulPortfolio(o: unknown): boolean {
326
+ const p = o as Events["portfolio"];
327
+ return (
328
+ (typeof p.accountId === "string" && p.accountId.includes("\0")) ||
329
+ (p.positions ?? []).some((e) => typeof e.symbol === "string" && e.symbol.includes("\0")) ||
330
+ (typeof p.updatedBy === "string" && p.updatedBy.includes("\0"))
331
+ );
332
+ }
333
+
334
+ function hasNulComplex(o: unknown): boolean {
335
+ const p = o as Events["complex"];
336
+ return (
337
+ (typeof p.id === "string" && p.id.includes("\0")) ||
338
+ (p.names ?? []).some((s) => typeof s === "string" && s.includes("\0"))
339
+ );
340
+ }
341
+
342
+ function hasNulBigVal(o: unknown): boolean {
343
+ const p = o as Events["bigVal"];
344
+ return (
345
+ (typeof p.id === "string" && p.id.includes("\0"))
346
+ );
347
+ }
348
+
349
+ function hasNulHello(o: unknown): boolean {
350
+ const p = o as ControlEvents["hello"];
351
+ return (
352
+ (p.caps ?? []).some((s) => typeof s === "string" && s.includes("\0"))
353
+ );
354
+ }
355
+
356
+ function hasNulWelcome(o: unknown): boolean {
357
+ const p = o as ControlEvents["welcome"];
358
+ return (
359
+ (typeof p.clientId === "string" && p.clientId.includes("\0")) ||
360
+ (p.groups ?? []).some((s) => typeof s === "string" && s.includes("\0"))
361
+ );
362
+ }
363
+
364
+ function hasNulSubscribe(o: unknown): boolean {
365
+ const p = o as ControlEvents["subscribe"];
366
+ return (
367
+ (typeof p.topic === "string" && p.topic.includes("\0"))
368
+ );
369
+ }
370
+
371
+ function hasNulUnsubscribe(o: unknown): boolean {
372
+ const p = o as ControlEvents["unsubscribe"];
373
+ return (
374
+ (typeof p.topic === "string" && p.topic.includes("\0"))
375
+ );
376
+ }
377
+
378
+ function hasNulJoinGroup(o: unknown): boolean {
379
+ const p = o as ControlEvents["joinGroup"];
380
+ return (
381
+ (typeof p.group === "string" && p.group.includes("\0"))
382
+ );
383
+ }
384
+
385
+ function hasNulLeaveGroup(o: unknown): boolean {
386
+ const p = o as ControlEvents["leaveGroup"];
387
+ return (
388
+ (typeof p.group === "string" && p.group.includes("\0"))
389
+ );
390
+ }
391
+
392
+ function hasNulSnapshotRequest(o: unknown): boolean {
393
+ const p = o as ControlEvents["snapshotRequest"];
394
+ return (
395
+ (typeof p.topic === "string" && p.topic.includes("\0"))
396
+ );
397
+ }
398
+
399
+ function hasNulPing(o: unknown): boolean {
400
+ const p = o as ControlEvents["ping"];
401
+ return false;
402
+ }
403
+
404
+ function hasNulPong(o: unknown): boolean {
405
+ const p = o as ControlEvents["pong"];
406
+ return false;
407
+ }
408
+
409
+ /** per-event NUL pre-scan — true routes the payload to the JSON path (cstring truncates `\0`). */
410
+ export const hasNulEncoders: Partial<Record<AnyEventName, (o: unknown) => boolean>> = {
411
+ quote: hasNulQuote,
412
+ trade: hasNulTrade,
413
+ portfolio: hasNulPortfolio,
414
+ complex: hasNulComplex,
415
+ bigVal: hasNulBigVal,
416
+ hello: hasNulHello,
417
+ welcome: hasNulWelcome,
418
+ subscribe: hasNulSubscribe,
419
+ unsubscribe: hasNulUnsubscribe,
420
+ joinGroup: hasNulJoinGroup,
421
+ leaveGroup: hasNulLeaveGroup,
422
+ snapshotRequest: hasNulSnapshotRequest,
423
+ ping: hasNulPing,
424
+ pong: hasNulPong,
425
+ };
426
+
427
+ export const directSymbolNames: Partial<Record<AnyEventName, string>> = {
428
+ quote: "fb_quote_serialize",
429
+ trade: "fb_trade_serialize",
430
+ portfolio: "fb_portfolio_serialize",
431
+ complex: "fb_complex_serialize",
432
+ bigVal: "fb_big_val_serialize",
433
+ hello: "fb_hello_serialize",
434
+ welcome: "fb_welcome_serialize",
435
+ subscribe: "fb_subscribe_serialize",
436
+ unsubscribe: "fb_unsubscribe_serialize",
437
+ joinGroup: "fb_join_group_serialize",
438
+ leaveGroup: "fb_leave_group_serialize",
439
+ snapshotRequest: "fb_snapshot_request_serialize",
440
+ ping: "fb_ping_serialize",
441
+ pong: "fb_pong_serialize",
442
+ };
443
+
444
+ export type DirectEncoder = (call: DirectCall, o: unknown, out: Uint8Array) => number;
445
+
446
+ export const directEncoders: Partial<Record<AnyEventName, DirectEncoder>> = {
447
+ quote: encodeQuote,
448
+ trade: encodeTrade,
449
+ portfolio: encodePortfolio,
450
+ complex: encodeComplex,
451
+ bigVal: encodeBigVal,
452
+ hello: encodeHello,
453
+ welcome: encodeWelcome,
454
+ subscribe: encodeSubscribe,
455
+ unsubscribe: encodeUnsubscribe,
456
+ joinGroup: encodeJoinGroup,
457
+ leaveGroup: encodeLeaveGroup,
458
+ snapshotRequest: encodeSnapshotRequest,
459
+ ping: encodePing,
460
+ pong: encodePong,
461
+ };
462
+
463
+ /**
464
+ * Bind-time self-test for the direct fast-path symbols. Encodes each
465
+ * sample into `scratch` and verifies the frame invariant
466
+ * `out[0]` = WIRE_VERSION, `out[1..5]` = event id AND
467
+ * `bytes_written === WIRE_HEADER_LEN + 4 + size_prefix`
468
+ * (envelope header + 4-byte size prefix + flatbuffer). A signature drift,
469
+ * ABI mismatch, or encoder bug surfaces here instead of under load.
470
+ * Returns the list of symbol names to DISABLE (route via JSON path).
471
+ *
472
+ * `raw` must be the adapted symbol map (mode-aware) from ffi.ts.
473
+ */
474
+ export function directSelfTest(raw: Record<string, (...args: unknown[]) => number>, scratch: Uint8Array): string[] {
475
+ const disabled: string[] = [];
476
+ const dv = new DataView(scratch.buffer, scratch.byteOffset, scratch.byteLength);
477
+ for (const [name, sample] of Object.entries(directSamples)) {
478
+ const symName = directSymbolNames[name as AnyEventName];
479
+ const enc = directEncoders[name as AnyEventName];
480
+ if (!symName || !enc) continue;
481
+ const call = raw[symName];
482
+ if (!call) { disabled.push(symName); continue; }
483
+ try {
484
+ const w = enc(call, sample, scratch) as number;
485
+ if (w === 0) { disabled.push(symName); continue; }
486
+ if (w > scratch.byteLength) continue; // needed-size path — not a failure
487
+ const size = dv.getUint32(scratch.byteOffset + WIRE_HEADER_LEN, true);
488
+ const gotId = dv.getUint32(scratch.byteOffset + 1, true);
489
+ if (scratch[0] !== WIRE_VERSION || gotId !== anyEventNameToId[name] || w !== WIRE_HEADER_LEN + 4 + size) disabled.push(symName);
490
+ } catch {
491
+ disabled.push(symName);
492
+ }
493
+ }
494
+ return disabled;
495
+ }
@@ -0,0 +1,139 @@
1
+ // @generated by scripts/typebox-to-fbs.ts — DO NOT EDIT
2
+ // FlatBuffers schema derived from the TypeBox events registry (schema/index.ts).
3
+
4
+ file_identifier "IGNX";
5
+
6
+ enum Side : int32 {
7
+ BUY = 0,
8
+ SELL = 1,
9
+ }
10
+
11
+ enum Tags : int32 {
12
+ HOT = 0,
13
+ NEW = 1,
14
+ SALE = 2,
15
+ }
16
+
17
+ table Quote {
18
+ symbol:string;
19
+ bid:double;
20
+ ask:double;
21
+ bid_size:int64;
22
+ ask_size:int64;
23
+ ts:int64;
24
+ }
25
+
26
+ table Trade {
27
+ symbol:string;
28
+ price:double;
29
+ volume:int64;
30
+ side:Side;
31
+ ts:int64;
32
+ }
33
+
34
+ table PortfolioSnapshot {
35
+ account_id:string;
36
+ positions:[PortfolioPosition];
37
+ total_value:double;
38
+ cash:double;
39
+ ts:int64;
40
+ updated_by:string;
41
+ }
42
+
43
+ table PortfolioPosition {
44
+ symbol:string;
45
+ quantity:int64;
46
+ avg_price:double;
47
+ pnl:double;
48
+ }
49
+
50
+ table Complex {
51
+ id:string;
52
+ names:[string];
53
+ prices:[double];
54
+ counts:[int64];
55
+ flags:[bool];
56
+ tags:[Tags];
57
+ active:bool;
58
+ total:double;
59
+ ts:int64;
60
+ }
61
+
62
+ table Order {
63
+ order_id:string;
64
+ customer:Customer;
65
+ lines:[OrderLine];
66
+ notes:[string];
67
+ discounts:[double];
68
+ active:bool;
69
+ created_at:int64;
70
+ billing:OrderBilling;
71
+ }
72
+
73
+ table Customer {
74
+ id:string;
75
+ name:string;
76
+ vip:bool;
77
+ loyalty_points:int64;
78
+ rating:double;
79
+ }
80
+
81
+ table OrderLine {
82
+ sku:string;
83
+ qty:int64;
84
+ unit_price:double;
85
+ tags:[Tags];
86
+ }
87
+
88
+ table OrderBilling {
89
+ id:string;
90
+ name:string;
91
+ vip:bool;
92
+ loyalty_points:int64;
93
+ rating:double;
94
+ }
95
+
96
+ table BigVal {
97
+ id:string;
98
+ seq:int64;
99
+ when:int64;
100
+ }
101
+
102
+ table Hello {
103
+ version:int64;
104
+ caps:[string];
105
+ last_seq:int64;
106
+ }
107
+
108
+ table Welcome {
109
+ client_id:string;
110
+ groups:[string];
111
+ }
112
+
113
+ table Subscribe {
114
+ topic:string;
115
+ }
116
+
117
+ table Unsubscribe {
118
+ topic:string;
119
+ }
120
+
121
+ table JoinGroup {
122
+ group:string;
123
+ }
124
+
125
+ table LeaveGroup {
126
+ group:string;
127
+ }
128
+
129
+ table SnapshotRequest {
130
+ topic:string;
131
+ }
132
+
133
+ table Ping {
134
+ ts:int64;
135
+ }
136
+
137
+ table Pong {
138
+ ts:int64;
139
+ }