@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.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/docs/architecture.md +146 -0
- package/docs/publishing.md +119 -0
- package/docs/wire-format.md +170 -0
- package/index.ts +61 -0
- package/package.json +89 -0
- package/prebuilds/linux-x64/libignex_ffi.so +0 -0
- package/public/client.ts +23 -0
- package/public/nats.ts +19 -0
- package/public/server.ts +35 -0
- package/rust/Cargo.toml +19 -0
- package/rust/src/ffi.rs +135 -0
- package/rust/src/generated/backend.rs +2817 -0
- package/rust/src/generated/mod.rs +2 -0
- package/rust/src/lib.rs +9 -0
- package/rust/src/transcode/generated.rs +1352 -0
- package/rust/src/transcode/mod.rs +2 -0
- package/src/bridge/nats.ts +269 -0
- package/src/bridge/subjects.ts +30 -0
- package/src/core/auth.ts +56 -0
- package/src/core/backpressure.ts +39 -0
- package/src/core/client-heartbeat.ts +27 -0
- package/src/core/client-reconnect.ts +35 -0
- package/src/core/client-state.ts +76 -0
- package/src/core/client-wire.ts +72 -0
- package/src/core/client.ts +176 -0
- package/src/core/groups.ts +52 -0
- package/src/core/int64-guard.ts +44 -0
- package/src/core/metrics.ts +105 -0
- package/src/core/outbound.ts +76 -0
- package/src/core/replay.ts +31 -0
- package/src/core/ring.ts +85 -0
- package/src/core/rooms.ts +44 -0
- package/src/core/routing.ts +94 -0
- package/src/core/server.ts +294 -0
- package/src/core/state.ts +179 -0
- package/src/generated/direct-ser.ts +495 -0
- package/src/generated/fbs/backend.fbs +139 -0
- package/src/generated/registry.ts +341 -0
- package/src/generated/rust/backend_generated.rs +2817 -0
- package/src/generated/ts/backend.ts +25 -0
- package/src/generated/ts/big-val.ts +106 -0
- package/src/generated/ts/complex.ts +303 -0
- package/src/generated/ts/customer.ts +137 -0
- package/src/generated/ts/hello.ts +123 -0
- package/src/generated/ts/join-group.ts +78 -0
- package/src/generated/ts/leave-group.ts +78 -0
- package/src/generated/ts/order-billing.ts +137 -0
- package/src/generated/ts/order-line.ts +144 -0
- package/src/generated/ts/order.ts +236 -0
- package/src/generated/ts/ping.ts +74 -0
- package/src/generated/ts/pong.ts +74 -0
- package/src/generated/ts/portfolio-position.ts +120 -0
- package/src/generated/ts/portfolio-snapshot.ts +170 -0
- package/src/generated/ts/quote.ts +148 -0
- package/src/generated/ts/side.ts +8 -0
- package/src/generated/ts/snapshot-request.ts +78 -0
- package/src/generated/ts/subscribe.ts +78 -0
- package/src/generated/ts/tags.ts +9 -0
- package/src/generated/ts/trade.ts +135 -0
- package/src/generated/ts/unsubscribe.ts +78 -0
- package/src/generated/ts/welcome.ts +112 -0
- package/src/generated/ts-ser.ts +465 -0
- package/src/generated/wire-registry.json +20 -0
- package/src/native/codec.ts +35 -0
- package/src/native/ffi.ts +214 -0
- package/src/native/loader.ts +55 -0
- package/src/schema/index.ts +217 -0
- package/src/server.ts +87 -0
- package/src/transport/byte-buffer-pool.ts +63 -0
- package/src/transport/scratch.ts +48 -0
- package/src/transport/stats.ts +44 -0
- package/src/transport/transport.ts +106 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode-path statistics (direct vs JSON) accumulated per event name across the
|
|
3
|
+
* process. `createStats()` returns a small counter object; one instance lives
|
|
4
|
+
* in `transport.ts` and is surfaced via `getEncodeStats()` / `server.metrics()`.
|
|
5
|
+
*
|
|
6
|
+
* `bump` is O(1) on the hot path: each (event, path) pair owns a tiny mutable
|
|
7
|
+
* counter box that is allocated once (lazily), so a bump is a single number
|
|
8
|
+
* increment — no Map.get + Map.set churn per encode.
|
|
9
|
+
*/
|
|
10
|
+
export interface EncodeStats {
|
|
11
|
+
bump(name: string, path: "direct" | "json"): void;
|
|
12
|
+
get(): { direct: Record<string, number>; json: Record<string, number> };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Mutable counter box — allocated once per (event, path), incremented in place. */
|
|
16
|
+
interface Counter {
|
|
17
|
+
n: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toObj(m: Map<string, Counter>): Record<string, number> {
|
|
21
|
+
const out: Record<string, number> = {};
|
|
22
|
+
for (const [name, c] of m) out[name] = c.n;
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createStats(): EncodeStats {
|
|
27
|
+
const direct = new Map<string, Counter>();
|
|
28
|
+
const json = new Map<string, Counter>();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
bump(name, path) {
|
|
32
|
+
const m = path === "direct" ? direct : json;
|
|
33
|
+
let c = m.get(name);
|
|
34
|
+
if (!c) {
|
|
35
|
+
c = { n: 0 };
|
|
36
|
+
m.set(name, c);
|
|
37
|
+
}
|
|
38
|
+
c.n++;
|
|
39
|
+
},
|
|
40
|
+
get() {
|
|
41
|
+
return { direct: toObj(direct), json: toObj(json) };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal transport: JS object → wire frame via Rust FFI.
|
|
3
|
+
* frame = `[1-byte event_id][size-prefixed FlatBuffer]` — fully produced by Rust.
|
|
4
|
+
*
|
|
5
|
+
* Two paths:
|
|
6
|
+
* - DIRECT (flat events, generated): fields pushed straight into Rust as FFI
|
|
7
|
+
* args from a zero-alloc encoder — no JSON, no intermediate array, and a
|
|
8
|
+
* single reusable output scratch (`encodeToScratch`).
|
|
9
|
+
* - JSON fallback (events with vectors / nested tables): object → JSON →
|
|
10
|
+
* `fb_serialize` → Rust parses and builds (still allocates — documented).
|
|
11
|
+
*/
|
|
12
|
+
import { directEncoders, directSymbolNames, hasNulEncoders } from "../generated/direct-ser";
|
|
13
|
+
import { anyEventNameToId } from "../generated/registry";
|
|
14
|
+
import type { AnyEventName } from "../schema";
|
|
15
|
+
import { getDirectSymbol, getFfi } from "../native/ffi";
|
|
16
|
+
import { createScratch, MIN_CAP } from "./scratch";
|
|
17
|
+
import { createStats } from "./stats";
|
|
18
|
+
|
|
19
|
+
// Single reusable output scratch + encode-path stats, created once per process
|
|
20
|
+
// and reused for every encode (the zero-alloc hot path). Safe to reuse right
|
|
21
|
+
// after `ws.send` — Bun copies binary frames synchronously (verified
|
|
22
|
+
// empirically). These are intentionally module-level singletons: threading
|
|
23
|
+
// them through every encode call would only add parameter churn to the hot
|
|
24
|
+
// path for no functional gain.
|
|
25
|
+
const scratch = createScratch();
|
|
26
|
+
const stats = createStats();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolved direct-path record for one event — populated lazily on first encode
|
|
30
|
+
* and immutable afterwards (a direct symbol that was disabled by the bind-time
|
|
31
|
+
* self-test never re-enables, so caching the resolved call is safe).
|
|
32
|
+
*/
|
|
33
|
+
interface ResolvedDirect {
|
|
34
|
+
/** generated zero-alloc encoder (absent for JSON-only events) */
|
|
35
|
+
encoder?: (call: (...args: unknown[]) => number, o: unknown, out: Uint8Array) => number;
|
|
36
|
+
/** resolved FFI symbol (undefined = symbol disabled → JSON fallback) */
|
|
37
|
+
call?: (...args: unknown[]) => number;
|
|
38
|
+
/** per-event NUL pre-scan (absent when the event has no string fields) */
|
|
39
|
+
hasNul?: (o: unknown) => boolean;
|
|
40
|
+
}
|
|
41
|
+
const resolvedDirect = new Map<string, ResolvedDirect>();
|
|
42
|
+
|
|
43
|
+
function resolveDirect(name: AnyEventName): ResolvedDirect {
|
|
44
|
+
let r = resolvedDirect.get(name);
|
|
45
|
+
if (r === undefined) {
|
|
46
|
+
r = {};
|
|
47
|
+
const encoder = directEncoders[name];
|
|
48
|
+
if (encoder) {
|
|
49
|
+
r.encoder = encoder;
|
|
50
|
+
r.call = getDirectSymbol(directSymbolNames[name]!);
|
|
51
|
+
r.hasNul = hasNulEncoders[name];
|
|
52
|
+
}
|
|
53
|
+
resolvedDirect.set(name, r);
|
|
54
|
+
}
|
|
55
|
+
return r;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Zero-allocation encode into the shared output scratch. The returned view is
|
|
60
|
+
* only valid until the next call — publish/send it immediately (Bun copies).
|
|
61
|
+
* Accepts app events AND control events (hello/subscribe/ping/...) — the
|
|
62
|
+
* server encodes both through the Rust FFI.
|
|
63
|
+
*/
|
|
64
|
+
export function encodeToScratch(name: AnyEventName, payload: unknown): Uint8Array {
|
|
65
|
+
const r = resolveDirect(name);
|
|
66
|
+
const encoder = r.encoder;
|
|
67
|
+
if (encoder && r.call) {
|
|
68
|
+
// `call` is undefined when the bind-time self-test disabled the symbol —
|
|
69
|
+
// fall through to the JSON path (graceful degradation). Embedded NULs route
|
|
70
|
+
// to JSON too: the `cstring` direct path truncates them (silent data loss),
|
|
71
|
+
// the JSON path preserves them exactly.
|
|
72
|
+
if (!(r.hasNul?.(payload) ?? false)) {
|
|
73
|
+
scratch.grow(MIN_CAP);
|
|
74
|
+
const w = scratch.neededSize(name, encoder(r.call, payload, scratch.view), () => encoder(r.call!, payload, scratch.view));
|
|
75
|
+
stats.bump(name, "direct");
|
|
76
|
+
return scratch.view.subarray(0, w);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// JSON fallback (vector/nested events, or a disabled direct symbol).
|
|
81
|
+
const id = anyEventNameToId[name];
|
|
82
|
+
if (id === undefined) throw new Error(`ignex: unknown event "${name}"`);
|
|
83
|
+
|
|
84
|
+
const json = JSON.stringify(payload);
|
|
85
|
+
const ffi = getFfi();
|
|
86
|
+
scratch.grow(Math.max(MIN_CAP, json.length * 2 + 128));
|
|
87
|
+
const w = scratch.neededSize(name, ffi.fb_serialize(id, json, scratch.view), () => ffi.fb_serialize(id, json, scratch.view));
|
|
88
|
+
stats.bump(name, "json");
|
|
89
|
+
return scratch.view.subarray(0, w);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── encode-path stats (direct vs JSON) ─────────────────────────────────
|
|
93
|
+
// Accumulated per event name across the process (typical deployments run one
|
|
94
|
+
// server per process). Surfaced via `getEncodeStats()` / `server.metrics()`.
|
|
95
|
+
|
|
96
|
+
export function getEncodeStats(): { direct: Record<string, number>; json: Record<string, number> } {
|
|
97
|
+
return stats.get();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Owned copy (safe to hold) — used by tests/bench. One allocation. */
|
|
101
|
+
export function encodeEvent(name: AnyEventName, payload: unknown): Uint8Array {
|
|
102
|
+
const frame = encodeToScratch(name, payload);
|
|
103
|
+
const owned = new Uint8Array(frame.byteLength);
|
|
104
|
+
owned.set(frame);
|
|
105
|
+
return owned;
|
|
106
|
+
}
|