@ignex/nova 0.1.1 → 0.1.3

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 (67) hide show
  1. package/README.md +132 -32
  2. package/docs/ai/LOCAL_DEV.md +81 -0
  3. package/docs/ai/TREE.md +232 -0
  4. package/docs/architecture.md +35 -10
  5. package/docs/events.md +170 -0
  6. package/docs/generic-bindings.md +197 -0
  7. package/docs/publishing.md +2 -2
  8. package/docs/wire-format.md +9 -2
  9. package/index.ts +75 -27
  10. package/package.json +12 -2
  11. package/prebuilds/linux-x64/libignex_ffi.so +0 -0
  12. package/public/bindings.ts +24 -0
  13. package/public/client.ts +5 -1
  14. package/public/events.ts +71 -0
  15. package/public/generate.ts +416 -0
  16. package/public/internal.ts +16 -0
  17. package/public/nats.ts +9 -5
  18. package/public/server.ts +42 -16
  19. package/rust/src/ffi.rs +10 -0
  20. package/rust/src/transcode/generated.rs +2 -1
  21. package/src/bindings/assemble.ts +73 -0
  22. package/src/bindings/default.ts +65 -0
  23. package/src/bindings/types.ts +113 -0
  24. package/src/bridge/nats.ts +53 -13
  25. package/src/bridge/subjects.ts +3 -0
  26. package/src/codegen/constants.ts +18 -0
  27. package/src/codegen/direct-gen.ts +550 -0
  28. package/src/codegen/fingerprint.ts +44 -0
  29. package/src/codegen/hash.ts +25 -0
  30. package/src/codegen/registry-gen.ts +242 -0
  31. package/src/codegen/rust-glue-gen.ts +545 -0
  32. package/src/codegen/schema-model.ts +338 -0
  33. package/src/codegen/ts-ser-gen.ts +221 -0
  34. package/src/codegen/typebox-to-fbs.ts +60 -0
  35. package/src/core/auth.ts +2 -1
  36. package/src/core/client-heartbeat.ts +2 -1
  37. package/src/core/client-reconnect.ts +9 -2
  38. package/src/core/client-state.ts +21 -8
  39. package/src/core/client-wire.ts +10 -11
  40. package/src/core/client.ts +34 -29
  41. package/src/core/groups.ts +3 -0
  42. package/src/core/metrics.ts +7 -3
  43. package/src/core/outbound.ts +12 -5
  44. package/src/core/routing.ts +17 -8
  45. package/src/core/server.ts +108 -34
  46. package/src/core/state.ts +51 -13
  47. package/src/events/clients.ts +156 -0
  48. package/src/events/cluster.ts +732 -0
  49. package/src/events/data.ts +38 -0
  50. package/src/events/emit.ts +127 -0
  51. package/src/events/global.ts +117 -0
  52. package/src/events/groups.ts +118 -0
  53. package/src/events/hub.ts +481 -0
  54. package/src/events/index.ts +61 -0
  55. package/src/events/queue.ts +96 -0
  56. package/src/events/registry.ts +178 -0
  57. package/src/events/types.ts +378 -0
  58. package/src/generated/direct-ser.ts +2 -1
  59. package/src/generated/fbs/backend.fbs +1 -1
  60. package/src/generated/registry.ts +3 -1
  61. package/src/generated/ts-ser.ts +1 -1
  62. package/src/generated/wire-registry.json +1 -0
  63. package/src/native/ffi.ts +85 -28
  64. package/src/schema/index.ts +5 -2
  65. package/src/server.ts +7 -3
  66. package/src/transport/stats.ts +8 -4
  67. package/src/transport/transport.ts +149 -68
package/docs/events.md ADDED
@@ -0,0 +1,170 @@
1
+ # Events layer — typed event-driven system on the FlatBuffer core
2
+
3
+ The events layer (`@ignex/nova/events`, opt-in via `createServer({ events })`) is
4
+ the application-facing, event-driven surface on top of the transport: **an
5
+ events file receives events, and a global emit sends events through
6
+ websockets** — with first-class client records, named groups, and
7
+ cross-instance sync for horizontally scaled deployments.
8
+
9
+ Enable it (everything else is opt-in):
10
+
11
+ ```ts
12
+ import { createServer } from "@ignex/nova/server";
13
+
14
+ const server = createServer({
15
+ port: 3000,
16
+ events: {
17
+ onConnect: (client) => client.data.set("connectedAt", client.connectedAt),
18
+ },
19
+ });
20
+ ```
21
+
22
+ `server.events` is the hub; the module-global singleton
23
+ (`@ignex/nova/events`) is bound to it by default.
24
+
25
+ ## The events file (receiving events)
26
+
27
+ Declare handlers like routes — a dedicated events file where inbound events
28
+ arrive. The handler receives the payload and a context describing **who sent
29
+ it and how to reply**:
30
+
31
+ ```ts
32
+ // app/events.ts
33
+ import { on, emitToUser, emitToGroup } from "@ignex/nova/events";
34
+
35
+ on("chat.message", (payload, ctx) => {
36
+ // ctx.client — the sender's connection record (id, userId, data, groups…)
37
+ // ctx.emit / ctx.emitToUser / … — reply without importing the singleton
38
+ emitToUser(payload.to, "chat.delivered", { id: payload.id });
39
+ });
40
+
41
+ on("order.created", (payload, ctx) => {
42
+ emitToGroup("backoffice", "order.alert", { orderId: payload.orderId });
43
+ });
44
+ ```
45
+
46
+ - `hub.on(name, handler)` / `off` / `once` / `onAny` — multiple handlers per
47
+ event, per-handler error isolation (one throwing handler never blocks the
48
+ others; failures count in `metrics().handlerErrors`).
49
+ - `hub.onServerEvent(name, handler)` — server-side handling of events that
50
+ arrive from OTHER instances or the NATS bridge (`ctx.source` is
51
+ `"remote"`/`"bridge"`, `ctx.client` is undefined). Do NOT re-emit the same
52
+ event from these handlers (loop).
53
+ - `on` auto-allows the event for inbound clients (`server.allowInbound`);
54
+ `onAny` sees every event listed in `events.inbound`.
55
+
56
+ ## The global emit (sending events through websockets)
57
+
58
+ `emit` and friends are importable anywhere — no server reference needed:
59
+
60
+ ```ts
61
+ import { emit, emitToGroup, emitToUser, emitToClient, emitToTopic } from "@ignex/nova/events";
62
+
63
+ emit("quote", { symbol: "AAPL", bid: 180.1, ask: 180.2 }); // broadcast
64
+ emitToGroup("traders", "alert", { text: "halt" }); // group fan-out
65
+ emitToUser("u-42", "order.update", { orderId: "o-1" }); // user's sockets
66
+ emitToClient("c-123", "session.expired", { reason: "idle" }); // one connection
67
+ ```
68
+
69
+ Or the discriminated `EmitTarget` — the API's way of **differentiating**
70
+ between addressing modes:
71
+
72
+ ```ts
73
+ server.events.emit("quote", payload, { type: "group", group: "traders" });
74
+ server.events.emit("quote", payload, { type: "user", userId: "u-42" });
75
+ ```
76
+
77
+ When the cluster is configured, every emit is cluster-aware: the target is
78
+ matched against clients on ALL instances.
79
+
80
+ ## Client records — who is connected, on whose behalf
81
+
82
+ Each active connection is a `client` record:
83
+
84
+ ```ts
85
+ interface EventClient {
86
+ id: string; // connection id (unique per socket)
87
+ userId?: string; // identity this connection acts ON BEHALF OF
88
+ meta?: Record<string, unknown>; // auth metadata
89
+ data: ClientData; // per-connection app store (auto-cleared on close)
90
+ groups: ReadonlySet<string>; // client groups (shared with ws.data)
91
+ topics: ReadonlySet<string>; // joined topics (shared with ws.data)
92
+ connectedAt: number; ip: string; closed: boolean; ws: ServerWebSocket;
93
+ }
94
+ ```
95
+
96
+ - `userId` — "on what behalf": set from `authenticate` (`{ userId }`),
97
+ `hub.setUserId(clientId, userId)`, or later. Several sockets may share one
98
+ `userId` (multi-tab / multi-device); `hub.clientsByUser(userId)` groups
99
+ them and `emitToUser` reaches all of them.
100
+ - `data` — per-connection state (`client.data.set(key, value)` or
101
+ `hub.setClientData(clientId, key, value)`); with a shared state store it
102
+ syncs cluster-wide (`hub.remoteClientData(clientId)`).
103
+ - `hub.client(id)` / `clients()` / `clientCount` — live introspection.
104
+ - Lifecycle hooks: `events.onConnect(client)` (seed data) and
105
+ `events.onDisconnect(client)`.
106
+
107
+ ## Groups — broadcast to groups vs individual users
108
+
109
+ Two group kinds, clearly differentiated:
110
+
111
+ | | `hub.group(name)` | `hub.userGroup(name)` |
112
+ |---|---|---|
113
+ | membership | connection ids | user ids |
114
+ | fan-out | members' sockets | every socket of each member user |
115
+ | shares transport groups | yes (`ws.data.groups`, control frames, `server.joinGroup`) | hub-managed |
116
+ | cluster membership | shared state store (`clusterGroupMembers`) | shared state store (`clusterUserGroupMembers`) |
117
+
118
+ ```ts
119
+ const traders = server.events.group("traders"); // client group
120
+ traders.add(clientId); traders.remove(clientId);
121
+ traders.members(); traders.emit("quote", payload);
122
+
123
+ const ops = server.events.userGroup("ops"); // user group
124
+ ops.add("u-42"); ops.emit("alert", { text: "pager" }); // all of u-42's sockets
125
+ ```
126
+
127
+ ## Horizontal scaling (cluster sync)
128
+
129
+ When multiple instances share a broker, every emit is delivered to the
130
+ target's clients on every instance. Heavy work never runs on the hot path:
131
+ local delivery is synchronous (encode once via the transport scratch + `ws.send`),
132
+ everything else (broker publishes, state-store writes, presence maintenance) is
133
+ deferred to a bounded offload queue (`events.queue`, drop-newest on overflow).
134
+
135
+ - **NATS** (server ⇄ server): reuse the server bridge with
136
+ `cluster: { nats: true }`, or a dedicated/different bridge
137
+ (`nats: NatsBridgeOptions | NatsBridge`).
138
+ - **Redis**: `cluster: { redis: { url: "redis://…" } }` (lazy `ioredis`
139
+ optional peer dependency — `bun add ioredis`).
140
+ - **Custom**: `cluster: { transport: MyClusterTransport }` (tests use an
141
+ in-memory bus).
142
+ - Self-delivery dedupe: frames carry the origin `instanceId`
143
+ (`cluster.instanceId`, random by default); an instance drops its own frames,
144
+ so a broadcast is delivered exactly once per socket.
145
+ - **Presence with no shared state**: join/leave + periodic heartbeat messages
146
+ — `hub.clusterClients()` lists connections on other instances.
147
+ - **Shared state store** (`cluster.state`, default per-instance memory;
148
+ production: `createRedisStateStore(...)`): user→clients index
149
+ (`clusterUserClients`), cluster group membership, cluster-wide client data.
150
+ - **Server-side events**: other instances' events reach `onServerEvent`
151
+ handlers with `ctx.source === "remote"` (delivered to clients AND handlers);
152
+ NATS-inbound events reach them with `source === "bridge"`.
153
+
154
+ ## Metrics & shutdown
155
+
156
+ `server.getMetrics().events` (or `hub.metrics()`) exposes emitted counts per
157
+ target, delivered local frames, cluster received/self-dropped/errors,
158
+ queue and handler errors, presence sizes. `hub.close()` unsubscribes,
159
+ flushes the queue, announces leaves, and closes owned transports; call it via
160
+ `server.drain()` / `server.stop()`.
161
+
162
+ ## Performance notes
163
+
164
+ - Zero-alloc local encode + fan-out (the transport scratch is reused; Bun
165
+ copies on `ws.send`) — the emit call is O(target sockets).
166
+ - The cluster publish copies the frame once (required — the scratch is
167
+ reused) and enqueues; a slow or offline broker never blocks the socket loop
168
+ and never throws into it.
169
+ - Without `events`, there is zero overhead; the global functions throw a
170
+ descriptive error if no hub is bound.
@@ -0,0 +1,197 @@
1
+ # Generic bindings — bring your own schema
2
+
3
+ `@ignex/nova` is schema-driven: **any** TypeBox schema you define in your app can
4
+ be turned into a full wire stack (FlatBuffers schema, TS decoders, pure-JS
5
+ encoder, Rust FFI fast path, NATS wire registry) with one function call —
6
+ `generateBindings(schema)` — and then served / consumed / bridged through the
7
+ same `createServer` / `createClient` / `createNatsBridge` APIs, fully typed
8
+ against **your** events.
9
+
10
+ ```
11
+ your app
12
+ src/schema.ts (TypeBox — source of truth)
13
+ │ scripts/generate-bindings.ts
14
+ │ import { generateBindings } from "@ignex/nova/generate";
15
+
16
+ ignex/generated/ (backend.fbs, ts/decoders, registry.ts, ts-ser.ts,
17
+ direct-ser.ts, wire-registry.json, rust/ crate, index.ts)
18
+ │ makeBindings(yourSchema) → Bindings
19
+
20
+ createServer({ bindings }) · createClient(url, { bindings }) · createNatsBridge({ bindings })
21
+ ```
22
+
23
+ ## 1. Define your schema (TypeBox)
24
+
25
+ ```ts
26
+ // src/schema.ts — your app's single source of truth
27
+ import { Type } from "@sinclair/typebox";
28
+
29
+ export const ChatMsg = Type.Object(
30
+ { room: Type.String(), text: Type.String(), ts: Type.Integer() },
31
+ { additionalProperties: false },
32
+ );
33
+ export const Telemetry = Type.Object(
34
+ { device: Type.String(), readings: Type.Array(Type.Number()), ok: Type.Boolean() },
35
+ { additionalProperties: false },
36
+ );
37
+
38
+ export const schemas = { ChatMsg, Telemetry };
39
+ export const events = { chat: ChatMsg, telemetry: Telemetry };
40
+ export const controlEvents = {}; // optional extra transport-internal events
41
+ ```
42
+
43
+ Rules (same as the built-in registry):
44
+
45
+ - `Type.Object({...}, { additionalProperties: false })` for payloads.
46
+ - `Type.Integer()` → int64; `Type.Integer({ bigint: true })` / `Type.BigInt()`
47
+ → exact bigint int64 (lossless beyond 2^53).
48
+ - String-literal unions (`Type.Union([Type.Literal("a"), ...])`) → enums.
49
+ - Arrays of scalars / strings / enums / flat objects → vectors (packed on the
50
+ direct fast path). Nested objects → tables. Tables-in-tables fall back to the
51
+ JSON path.
52
+ - The transport control events (hello / welcome / subscribe / unsubscribe /
53
+ joinGroup / leaveGroup / snapshotRequest / ping / pong) are ALWAYS included —
54
+ you can add your own but cannot override the standard ones.
55
+
56
+ ## 2. Generate bindings
57
+
58
+ > `scripts/generate-bindings.ts` is an example filename **you** give a script in your own app — it is not a file shipped in the `@ignex/nova` package, so don't go looking for it in `node_modules`.
59
+
60
+ ```ts
61
+ // scripts/generate-bindings.ts — run once per schema change
62
+ import { generateBindings } from "@ignex/nova/generate";
63
+ import { schemas, events, controlEvents } from "../src/schema";
64
+
65
+ const gen = generateBindings(
66
+ { schemas, events, controlEvents },
67
+ { outDir: "./ignex/generated" }, // default
68
+ );
69
+ const written = gen.write(); // e.g. 26 files under ignex/generated/
70
+ console.log("generated:", written.length, "files");
71
+ ```
72
+
73
+ Requirements: `flatc` on PATH (the FlatBuffers compiler — the same prerequisite
74
+ as the built-in registry: `brew install flatbuffers` /
75
+ `apt install flatbuffers-compiler`). Pass `rust: false` to skip the Rust crate
76
+ scaffold (you lose the FFI fast path; the pure-JS encoder is still used).
77
+
78
+ The output folder contains:
79
+
80
+ | File | Role |
81
+ | --- | --- |
82
+ | `backend.fbs` | FlatBuffers schema (wire layout for independent consumers) |
83
+ | `ts/*.ts` | flatc-generated decoders (browser + Bun) |
84
+ | `registry.ts` | event ids, `readFrameHeader` / `decodePayload` / `decodeFrame`, `SCHEMA_FINGERPRINT` |
85
+ | `ts-ser.ts` | pure-JS encoder (works in the browser) |
86
+ | `direct-ser.ts` | direct fast-path serde (Bun server, when FFI is used) |
87
+ | `wire-registry.json` | machine-readable event-id registry for NATS consumers |
88
+ | `rust/` | a complete cargo crate — build it for the FFI fast path |
89
+ | `index.ts` | `makeBindings(schema)` — assembles the runtime `Bindings` |
90
+
91
+ ## 3. Assemble the bindings and use the APIs
92
+
93
+ ```ts
94
+ // bindings.ts
95
+ import { makeBindings } from "./ignex/generated"; // generated
96
+ import * as schema from "../src/schema";
97
+
98
+ export const bindings = makeBindings(schema);
99
+ export type AppEvents = import("@ignex/nova").EventsOf<typeof bindings>;
100
+ ```
101
+
102
+ ```ts
103
+ // server.ts (Bun)
104
+ import { createServer } from "@ignex/nova/server";
105
+ import { bindings } from "./bindings";
106
+
107
+ const server = createServer({
108
+ port: 3000,
109
+ bindings,
110
+ inbound: ["chat"], // events clients may send
111
+ nats: { servers: ["nats://localhost:4222"], inbound: true, bridgeClientEvents: true },
112
+ });
113
+ server.publish("chat", { room: "lobby", text: "hello", ts: Date.now() }); // typed!
114
+ server.on("chat", (msg, ws) => console.log(msg.room, msg.text));
115
+ ```
116
+
117
+ ```ts
118
+ // FE (browser or Bun)
119
+ import { createClient } from "@ignex/nova/client";
120
+ import { bindings } from "./bindings";
121
+
122
+ const client = createClient("ws://localhost:3000/ws", { bindings });
123
+ client.on("telemetry", (t) => console.log(t.device, t.readings)); // typed!
124
+ client.send("chat", { room: "lobby", text: "hi from FE", ts: Date.now() });
125
+ client.connect();
126
+ ```
127
+
128
+ The whole public surface is generic:
129
+
130
+ - `createServer({ bindings })` → `publish` / `publishToTopic` / `publishToGroup`
131
+ / `on` / … typed against your `Events`.
132
+ - `createClient(url, { bindings })` → `on` / `send` / `once` / `onAny` / …
133
+ typed against your `Events`.
134
+ - `createNatsBridge({ bindings })` → decodes inbound frames with your schema.
135
+
136
+ ## Rust FFI fast path (optional)
137
+
138
+ By default generated bindings use `ffiMode: "optional"`: the server tries the
139
+ Rust addon only when you point `IGNEX_FFI_PATH` at one — otherwise it silently
140
+ uses the pure-JS encoder (correct everywhere, just not zero-allocation). To get
141
+ the fast path:
142
+
143
+ ```bash
144
+ cd ignex/generated/rust
145
+ cargo build --release # produces libignex_ffi.so/.dylib/.dll
146
+ IGNEX_FFI_PATH=$(pwd)/target/release/libignex_ffi.so bun run your-server.ts
147
+ ```
148
+
149
+ The cdylib exports a **schema fingerprint** (`fb_schema_fingerprint`) that the
150
+ bind-time self-test checks against `SCHEMA_FINGERPRINT` in your generated
151
+ registry — a stale or schema-mismatched addon fails loudly instead of producing
152
+ undecodable frames. (`ffiMode: "required"` makes a missing/mismatched addon a
153
+ hard error.)
154
+
155
+ ## NATS & horizontal scaling
156
+
157
+ Point every server instance at the same NATS and the same subject prefix, and
158
+ they behave as one hub:
159
+
160
+ - Every `publish` / `publishToTopic` / `publishToGroup` is ALSO published to
161
+ NATS as the identical wire frame (`{prefix}.broadcast.<event>`,
162
+ `{prefix}.topic.<topic>.<event>`, `{prefix}.group.<group>.<event>`) — any
163
+ backend (BE) service can consume them with the wire registry +
164
+ `backend.fbs`.
165
+ - External producers (or BE services) publish on `{prefix}.inbound.>`; every
166
+ server forwards those events to its own clients.
167
+ - With `nats.bridgeClientEvents: true`, events that a client sends to ONE
168
+ server are re-published to `{prefix}.inbound.<event>`, so all other servers'
169
+ clients receive them too — client messages become cluster-wide. Loop
170
+ prevention is built in: frames that arrive via NATS are forwarded to clients
171
+ but never re-bridged.
172
+
173
+ ```
174
+ ┌─────────────┐ publish("chat", …) ┌─────────────┐
175
+ FE client ─▶│ server A │──────▶ NATS ◀─────────│ server B │◀─ FE client
176
+ │ ignex.* │ │ ignex.* │
177
+ └─────────────┘ └─────────────┘
178
+ │ inbound.<event> (client-sent, bridgeClientEvents)
179
+ └──────────────▶ BE consumers (any language)
180
+ ```
181
+
182
+ Because the wire bytes are schema-derived, all instances must run the SAME
183
+ generated bindings (same event names → same FNV-1a ids). Changing the schema
184
+ changes the fingerprint — regenerate all instances together.
185
+
186
+ ## Independent (non-JS) consumers
187
+
188
+ `wire-registry.json` maps event names → stable FNV-1a ids, and `backend.fbs`
189
+ is the FlatBuffer layout — decode bridged NATS frames in any language. See
190
+ `docs/wire-format.md` for the envelope.
191
+
192
+ ## What about the built-in events?
193
+
194
+ The built-in registry (quote/trade/portfolio/…) is just the default
195
+ `Bindings` (`defaultBindings`); every API accepts `bindings` and defaults to
196
+ it, so existing code is untouched. Use the built-in events as a reference for
197
+ schema style — your app's schema works exactly the same way.
@@ -10,9 +10,9 @@ mirrors how the `@ignex/*` packages in the Ignex monorepo are shipped.
10
10
  | Field | Value | Why |
11
11
  | --- | --- | --- |
12
12
  | `main` / `module` / `types` | `./index.ts` | source entrypoint (Bun-native) |
13
- | `exports` | `.` → `index.ts`, `./server` → `public/server.ts`, `./client` → `public/client.ts`, `./nats` → `public/nats.ts`, `./package.json` | typed subpath API |
13
+ | `exports` | `@ignex/nova` → `index.ts`; `@ignex/nova/server` → `public/server.ts`; `@ignex/nova/client` → `public/client.ts`; `@ignex/nova/nats` → `public/nats.ts`; `@ignex/nova/events` → `public/events.ts`; `@ignex/nova/bindings` → `public/bindings.ts`; `@ignex/nova/generate` → `public/generate.ts`; `@ignex/nova/internal` → `public/internal.ts`; `@ignex/nova/package.json` → `package.json` | typed subpath API |
14
14
  | `files` | `index.ts`, `public`, `src`, `rust`, `prebuilds`, `docs`, `README.md`, `LICENSE` | everything consumers need, nothing they don't |
15
- | `publishConfig` | `{ "access": "public" }` | unscoped package must be public |
15
+ | `publishConfig` | `{ "access": "public" }` | scoped packages are restricted by default — `access: public` publishes `@ignex/nova` publicly |
16
16
  | `engines` | `{ "bun": ">=1.4" }` | Bun-only runtime |
17
17
  | `sideEffects` | `false` | safe to tree-shake / mark in bundlers |
18
18
 
@@ -113,8 +113,8 @@ Bridge state is observable via `server.getMetrics()`
113
113
  ### Decoding bridged frames (external consumers)
114
114
 
115
115
  `bun run generate` emits `src/generated/wire-registry.json` — a machine-readable
116
- `{ version, events: { name: id } }` map of the FNV-1a ids. A consumer in any
117
- language:
116
+ `{ version, fingerprint, events: { name: id } }` map of the FNV-1a ids. A
117
+ consumer in any language:
118
118
 
119
119
  1. reads `version` (byte 0) and `event_id` (bytes 1..5) from the frame,
120
120
  2. maps `event_id` → name via `wire-registry.json`,
@@ -123,6 +123,13 @@ language:
123
123
 
124
124
  See `examples/nats-consumer.ts` for a working reference.
125
125
 
126
+ For YOUR OWN schema, `generateBindings` (see
127
+ [docs/generic-bindings.md](generic-bindings.md)) emits the same
128
+ `backend.fbs` + `wire-registry.json` into your project, plus a `fingerprint`
129
+ field — the cdylib and the generated registry share it, so a schema-mismatched
130
+ native addon fails the bind-time self-test instead of emitting undecodable
131
+ frames.
132
+
126
133
  ## Heartbeat
127
134
 
128
135
  Clients send `ping` every `heartbeatMs` (default 15000). The server replies
package/index.ts CHANGED
@@ -2,45 +2,61 @@
2
2
  * ignex-nova — public package root.
3
3
  *
4
4
  * Re-exports the full typed pub/sub API (server + client + NATS bridge +
5
- * schema types) so a single `import ... from "ignex-nova"` works in Bun
6
- * projects.
5
+ * schema types + generic bindings codegen) so a single `import ... from
6
+ * "@ignex/nova"` works in Bun projects.
7
7
  *
8
8
  * For leaner / target-specific imports use the subpath entrypoints — each
9
9
  * resolves to its own source file and tree-shakes independently:
10
10
  *
11
- * import { createServer } from "ignex-nova/server"; // Bun-only (Rust FFI)
12
- * import { createClient } from "ignex-nova/client"; // browser + Bun
13
- * import { createNatsBridge } from "ignex-nova/nats"; // standalone bridge
11
+ * import { createServer } from "@ignex/nova/server"; // Bun-only (Rust FFI)
12
+ * import { createClient } from "@ignex/nova/client"; // browser + Bun
13
+ * import { createNatsBridge } from "@ignex/nova/nats"; // standalone bridge
14
+ * import { on, emit } from "@ignex/nova/events"; // events layer + global emit
15
+ * import { generateBindings } from "@ignex/nova/generate"; // ANY-schema codegen
16
+ * import { assembleBindings, defaultBindings } from "@ignex/nova/bindings";
17
+ * import { encodeUtf8Into } from "@ignex/nova/internal"; // codegen helpers
14
18
  *
15
19
  * Note: the root entry also pulls in the Bun-only server + FFI path, so
16
- * browser bundles should import "ignex-nova/client" instead.
20
+ * browser bundles should import "@ignex/nova/client" instead.
17
21
  *
18
22
  * The README (and docs/publishing.md) covers consuming this from an npm
19
- * package: `bun add ignex-nova`, then use the subpaths above.
23
+ * package: `bun add @ignex/nova`, then use the subpaths above.
20
24
  */
21
- export { createServer } from "./public/server";
22
- export type {
23
- IgnServer,
24
- ClientInfo,
25
- IgnServerOptions,
26
- IgnBackpressureOptions,
27
- BackpressurePolicy,
28
- WsData,
29
- ClientMeta,
30
- AuthResult,
31
- MetricsSnapshot,
32
- Int64GuardMode,
33
- } from "./public/server";
34
25
 
35
- export { createClient } from "./public/client";
36
26
  export type {
27
+ AssembleOptions,
28
+ Bindings,
29
+ BindingsParts,
30
+ ControlEventNameOf,
31
+ ControlEventsOf,
32
+ DefaultBindings,
33
+ DirectCall,
34
+ DirectEncoder,
35
+ DirectTables,
36
+ EventNameOf,
37
+ EventsOf,
38
+ } from "./public/bindings";
39
+ export { assembleBindings, defaultBindings } from "./public/bindings";
40
+ export type {
41
+ ClientStatus,
37
42
  IgnClient,
38
43
  IgnClientOptions,
39
44
  IgnReconnectOptions,
40
- ClientStatus,
41
45
  } from "./public/client";
42
46
 
43
- export { createNatsBridge, createSubjectBuilder } from "./public/nats";
47
+ export { createClient } from "./public/client";
48
+ export type { GeneratedBindings, GenerateOptions, SchemaRegistry } from "./public/generate";
49
+ // ── generic bindings (ANY schema) ──────────────────────────────────────────
50
+ export { generateBindings } from "./public/generate";
51
+ // runtime helpers used by generated code (also exported via `@ignex/nova/internal`)
52
+ export {
53
+ checkInt64,
54
+ encodeUtf8Into,
55
+ ensureCapacity,
56
+ pooledByteBuffer,
57
+ setInt64GuardMode,
58
+ utf8Len,
59
+ } from "./public/internal";
44
60
  export type {
45
61
  NatsBridge,
46
62
  NatsBridgeOptions,
@@ -49,13 +65,45 @@ export type {
49
65
  NatsTransport,
50
66
  SubjectBuilder,
51
67
  } from "./public/nats";
68
+ export { createNatsBridge, createSubjectBuilder } from "./public/nats";
69
+ // events layer types (the runtime singleton API is `@ignex/nova/events`)
70
+ export type {
71
+ AuthResult,
72
+ BackpressurePolicy,
73
+ ClientData,
74
+ ClientGroup,
75
+ ClientInfo,
76
+ ClientMeta,
77
+ ClusterStateStore,
78
+ ClusterTransport,
79
+ EmitTarget,
80
+ EmitTargetKind,
81
+ EventClient,
82
+ EventContext,
83
+ EventHandler,
84
+ EventsClusterOptions,
85
+ EventsHub,
86
+ EventsMetricsSnapshot,
87
+ EventsOptions,
88
+ IgnBackpressureOptions,
89
+ IgnServer,
90
+ IgnServerOptions,
91
+ Int64GuardMode,
92
+ MetricsSnapshot,
93
+ RedisConnectionOptions,
94
+ RemoteClient,
95
+ ServerEventHandler,
96
+ UserGroup,
97
+ WsData,
98
+ } from "./public/server";
99
+ export { createServer } from "./public/server";
52
100
 
53
101
  // Plain-object payload types consumers type against (e.g. `Events["quote"]`).
54
102
  // Import as types only — the runtime `events` registry is transport-internal.
55
103
  export type {
56
- Events,
57
- EventName,
58
104
  AnyEventName,
59
- ControlEvents,
60
105
  ControlEventName,
61
- } from "./src/schema";
106
+ ControlEvents,
107
+ EventName,
108
+ Events,
109
+ } from "./src/schema";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignex/nova",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "TypeBox-driven FlatBuffer transport: Rust FFI serializer + Bun WebSocket + typed pub/sub API for server and FE.",
6
6
  "license": "MIT",
@@ -48,6 +48,10 @@
48
48
  "./server": "./public/server.ts",
49
49
  "./client": "./public/client.ts",
50
50
  "./nats": "./public/nats.ts",
51
+ "./events": "./public/events.ts",
52
+ "./bindings": "./public/bindings.ts",
53
+ "./generate": "./public/generate.ts",
54
+ "./internal": "./public/internal.ts",
51
55
  "./package.json": "./package.json"
52
56
  },
53
57
  "publishConfig": {
@@ -57,7 +61,7 @@
57
61
  "generate": "bun scripts/generate.ts",
58
62
  "build:rust": "cargo build --release --manifest-path rust/Cargo.toml",
59
63
  "build:client": "bun build ./client/main.ts --outdir ./client-dist --target=browser --splitting",
60
- "build:dist": "bun build ./index.ts ./public/server.ts ./public/client.ts ./public/nats.ts --outdir ./dist --target=bun --splitting && bunx tsc --emitDeclarationOnly -p tsconfig.dist.json",
64
+ "build:dist": "bun build ./index.ts ./public/server.ts ./public/client.ts ./public/nats.ts ./public/events.ts ./public/bindings.ts ./public/generate.ts ./public/internal.ts --outdir ./dist --target=bun --splitting && bunx tsc --emitDeclarationOnly -p tsconfig.dist.json",
61
65
  "build": "bun run generate && bun run build:rust && bun run build:client",
62
66
  "prebuild": "bun scripts/build-prebuild.ts",
63
67
  "test": "bun test",
@@ -65,6 +69,7 @@
65
69
  "typecheck": "bunx tsc --noEmit -p tsconfig.json",
66
70
  "verify": "bun run typecheck && bun run lint && bun test",
67
71
  "pack:check": "bun scripts/check-pack.ts",
72
+ "gen:ai-map": "bun scripts/gen-ai-map.ts",
68
73
  "serve": "bun run src/server.ts",
69
74
  "bench:serialize": "bun run bench/serialize.ts",
70
75
  "bench:throughput": "bun run bench/throughput.ts",
@@ -85,5 +90,10 @@
85
90
  },
86
91
  "peerDependencies": {
87
92
  "typescript": "^7"
93
+ },
94
+ "peerDependenciesMeta": {
95
+ "ioredis": {
96
+ "optional": true
97
+ }
88
98
  }
89
99
  }
Binary file
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Public bindings API — `@ignex/nova/bindings`.
3
+ *
4
+ * import { assembleBindings, defaultBindings } from "@ignex/nova/bindings";
5
+ * import type { Bindings, EventsOf } from "@ignex/nova/bindings";
6
+ *
7
+ * `defaultBindings` is the built-in registry's wire stack; `assembleBindings`
8
+ * builds a `Bindings` from generated parts (see `@ignex/nova/generate`).
9
+ */
10
+
11
+ export type { AssembleOptions, BindingsParts } from "../src/bindings/assemble";
12
+ export { assembleBindings } from "../src/bindings/assemble";
13
+ export { defaultBindings } from "../src/bindings/default";
14
+ export type {
15
+ Bindings,
16
+ ControlEventNameOf,
17
+ ControlEventsOf,
18
+ DefaultBindings,
19
+ DirectCall,
20
+ DirectEncoder,
21
+ DirectTables,
22
+ EventNameOf,
23
+ EventsOf,
24
+ } from "../src/bindings/types";
package/public/client.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * build stable). The implementation lives in the functional modules under
4
4
  * `src/core/`; this file just exposes the public surface.
5
5
  *
6
- * import { createClient } from "ignex-nova/client";
6
+ * import { createClient } from "@ignex/nova/client";
7
7
  *
8
8
  * const client = createClient("ws://localhost:3000/ws", { reconnect: true });
9
9
  * client.on("quote", (q) => { /* q: Events["quote"] — a plain object *\/ });
@@ -11,6 +11,10 @@
11
11
  * client.send("chat", {...}); // typed client→server (server must allow it)
12
12
  * client.connect();
13
13
  *
14
+ * // your own schema (see @ignex/nova/generate):
15
+ * const client = createClient("ws://localhost:3000/ws", { bindings });
16
+ * client.on("yourEvent", (e) => {...}); // typed against YOUR Events
17
+ *
14
18
  * Works in the browser (bundle with `bun build --target=browser`) AND in Bun.
15
19
  * Outgoing frames are encoded by the generated PURE-JS encoder — no Rust FFI
16
20
  * needed, so the browser can send too.