@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/README.md CHANGED
@@ -5,25 +5,59 @@ serializer — and a typed pub/sub API that hides all of it from developers.
5
5
 
6
6
  ```ts
7
7
  // server (Bun)
8
- import { createServer } from "ignex-nova/server";
9
- const server = createServer({ port: 3000, inbound: ["chat"] });
8
+ import { createServer } from "@ignex/nova/server";
9
+ const server = createServer({ port: 3000, inbound: ["trade"] });
10
10
  server.publish("quote", { symbol: "AAPL", bid: 180.1, ask: 180.2, bidSize: 100, askSize: 200, ts: Date.now() });
11
11
  server.publishToTopic("equities", "quote", {...}); // rooms
12
- server.on("chat", (msg, ws) => server.publishTo(ws, "chatAck", { ok: true }));
12
+ server.on("trade", (msg, ws) =>
13
+ server.publishTo(ws, "quote", { symbol: msg.symbol, bid: msg.price, ask: msg.price, bidSize: 1, askSize: 1, ts: Date.now() }),
14
+ );
13
15
 
14
16
  // FE (browser or Bun)
15
- import { createClient } from "ignex-nova/client";
17
+ import { createClient } from "@ignex/nova/client";
16
18
  const client = createClient("ws://localhost:3000/ws", { reconnect: true });
17
19
  client.on("quote", (q) => console.log(q.symbol, q.bid)); // q is a plain typed object
18
20
  client.subscribe("equities"); // rooms + last-value replay
19
- client.send("chat", { text: "hi" }); // typed client→server (pure-JS encoder — works in the browser)
21
+ client.send("trade", { symbol: "AAPL", price: 180.5, volume: 10, side: "buy", ts: Date.now() }); // typed client→server (pure-JS encoder — works in the browser)
20
22
  client.connect();
21
23
  ```
22
24
 
25
+ > The built-in registry ships `quote`/`trade`/`portfolio`/`complex`/`order`/`bigVal` — custom events like `chat` require your own schema via `generateBindings` (see below).
26
+
23
27
  No `flatbuffers.Builder`, no `getRootAs*`, no FFI — developers only ever see
24
28
  plain, type-checked objects. The FlatBuffer + Rust machinery is internal, and
25
29
  the **browser can send typed frames too** (via a generated pure-JS encoder).
26
30
 
31
+ ## Bring your own schema (generic bindings)
32
+
33
+ The transport is **schema-driven**: the built-in events (quote/trade/…) are
34
+ just the default registry. Define **any** TypeBox schema in your app and
35
+ `generateBindings(schema)` produces a complete, typed wire stack for it —
36
+ same APIs, same NATS story, your events:
37
+
38
+ ```ts
39
+ // scripts/generate-bindings.ts
40
+ import { generateBindings } from "@ignex/nova/generate";
41
+ import { schemas, events, controlEvents } from "../src/schema"; // YOUR TypeBox
42
+ generateBindings({ schemas, events, controlEvents }, { outDir: "./ignex/generated" }).write();
43
+
44
+ // bindings.ts
45
+ import { makeBindings } from "./ignex/generated";
46
+ import * as schema from "../src/schema";
47
+ export const bindings = makeBindings(schema);
48
+
49
+ // server.ts — publish/on typed against YOUR events; NATS bridging included
50
+ const server = createServer({ port: 3000, bindings, inbound: ["chat"],
51
+ nats: { servers: ["nats://localhost:4222"], inbound: true, bridgeClientEvents: true } });
52
+ server.publish("chat", { room: "lobby", text: "hi", ts: Date.now() });
53
+ ```
54
+
55
+ The generated stack (flatc TS decoders + pure-JS encoder + direct fast-path
56
+ serde + `wire-registry.json` for NATS consumers + an optional Rust crate for
57
+ the FFI fast path) works without a Rust toolchain — the server falls back to
58
+ the pure-JS encoder when no addon is present. Full guide:
59
+ [docs/generic-bindings.md](docs/generic-bindings.md).
60
+
27
61
  ## Features
28
62
 
29
63
  - **Typed pub/sub, both directions** — `publish`/`publishTo`/`publishToTopic`
@@ -46,12 +80,30 @@ the **browser can send typed frames too** (via a generated pure-JS encoder).
46
80
  server-side groups (`publishToGroup(group, …)`, joined via auth metadata,
47
81
  `joinGroup(id, group)`, or client `joinGroup` frames) target sets of clients.
48
82
  Active clients are listed via `getClients()` / `GET /clients`.
83
+ - **Generic, schema-driven** — `generateBindings(schema)` (from
84
+ `@ignex/nova/generate`) builds the whole wire stack for ANY TypeBox schema in
85
+ your app; `createServer` / `createClient` / `createNatsBridge` accept the
86
+ resulting `bindings` and are fully typed against your events. The built-in
87
+ events are just the default registry.
49
88
  - **NATS bridge (bidirectional)** — every broadcast/topic/group publish is also
50
89
  published to NATS as the **same FlatBuffer wire frame** (`ignex.broadcast.*`,
51
90
  `ignex.topic.*`, `ignex.group.*`) so other applications can consume it;
52
91
  external apps push events into the hub via `ignex.inbound.>` and the server
53
- forwards them to clients. Best-effort (never blocks the WS hot path),
54
- observable via metrics.
92
+ forwards them to clients. `bridgeClientEvents: true` re-publishes client-sent
93
+ events to the cluster (horizontal scaling). Best-effort (never blocks the WS
94
+ hot path), observable via metrics.
95
+ - **Events layer** (`@ignex/nova/events`, opt-in via `createServer({ events })`)
96
+ — the application-facing event-driven system on top of the transport: an
97
+ **events file** receives events (`server.events.on(...)` with a context
98
+ carrying the sender's client record), a **global emit** sends events through
99
+ websockets from anywhere (`emit` / `emitToGroup` / `emitToUser` /
100
+ `emitToClient`), **client records** model "who is connected, on whose behalf
101
+ (`userId`), and what to remember per connection (`client.data`)", **client
102
+ groups vs user groups** make group-vs-user targeting explicit, and an
103
+ optional **cluster sync** (NATS and/or Redis, offloaded from hot paths)
104
+ delivers every emit to the target's clients across horizontally scaled
105
+ instances, with presence + shared-state indexes. See
106
+ [docs/events.md](docs/events.md).
55
107
  - **Bun-only server, browser+Bun client.** Wire spec documented for independent
56
108
  clients: [docs/wire-format.md](docs/wire-format.md).
57
109
 
@@ -63,10 +115,10 @@ src/schema/index.ts (TypeBox — source of truth: app events + control events)
63
115
 
64
116
  backend.fbs ──flatc --ts──▶ src/generated/ts/ (decoders)
65
117
  └───flatc --rust──▶ rust/src/generated/ (table builders)
66
- scripts/rust-glue-gen.ts ─▶ rust/src/transcode/ (JSON glue + direct-args FFI)
67
- scripts/direct-gen.ts ────▶ src/generated/direct-ser.ts (direct fast-path serde)
68
- scripts/ts-ser-gen.ts ─────▶ src/generated/ts-ser.ts (pure-JS browser encoder)
69
- scripts/registry-gen.ts ───▶ src/generated/registry.ts (event routing, both sides)
118
+ src/codegen/rust-glue-gen.ts ─▶ rust/src/transcode/ (JSON glue + direct-args FFI)
119
+ src/codegen/direct-gen.ts ────▶ src/generated/direct-ser.ts (direct fast-path serde)
120
+ src/codegen/ts-ser-gen.ts ─────▶ src/generated/ts-ser.ts (pure-JS browser encoder)
121
+ src/codegen/registry-gen.ts ───▶ src/generated/registry.ts (event routing, both sides)
70
122
 
71
123
  server: JS object ─▶ (flat event) fields as direct FFI args ─▶ Rust
72
124
  └▶ (vector/nested) JSON.stringify → cstring ─▶ Rust
@@ -140,15 +192,15 @@ Published as **TypeScript source** (no build step needed — Bun runs `.ts`
140
192
  natively), with subpath entrypoints. Works in any Bun ≥ 1.4 project:
141
193
 
142
194
  ```bash
143
- bun add ignex-nova
195
+ bun add @ignex/nova
144
196
  ```
145
197
 
146
198
  ```ts
147
199
  // server (Bun-only — needs the native addon, see below)
148
- import { createServer } from "ignex-nova/server";
200
+ import { createServer } from "@ignex/nova/server";
149
201
 
150
202
  // client (browser + Bun) — the root entry also re-exports everything
151
- import { createClient, type Events } from "ignex-nova/client";
203
+ import { createClient, type Events } from "@ignex/nova/client";
152
204
 
153
205
  const q: Events["quote"] = { symbol: "AAPL", bid: 180.1, ask: 180.2, bidSize: 100, askSize: 200, ts: Date.now() };
154
206
  ```
@@ -157,17 +209,22 @@ Entrypoints:
157
209
 
158
210
  | Import | Resolves to |
159
211
  | --- | --- |
160
- | `ignex-nova` | `index.ts` — everything (server + client + nats + schema types) |
161
- | `ignex-nova/server` | `public/server.ts` — `createServer` (Bun-only) |
162
- | `ignex-nova/client` | `public/client.ts` — `createClient` (browser + Bun) |
163
- | `ignex-nova/nats` | `public/nats.ts` — standalone `createNatsBridge` |
212
+ | `@ignex/nova` | `index.ts` — everything (server + client + nats + schema types + generic codegen) |
213
+ | `@ignex/nova/server` | `public/server.ts` — `createServer` (Bun-only) |
214
+ | `@ignex/nova/client` | `public/client.ts` — `createClient` (browser + Bun) |
215
+ | `@ignex/nova/nats` | `public/nats.ts` — standalone `createNatsBridge` |
216
+ | `@ignex/nova/events` | `public/events.ts` — events layer + global emit |
217
+ | `@ignex/nova/generate` | `public/generate.ts` — `generateBindings` (ANY-schema codegen) |
218
+ | `@ignex/nova/bindings` | `public/bindings.ts` — `assembleBindings` / `defaultBindings` + types |
219
+ | `@ignex/nova/internal` | `public/internal.ts` — runtime helpers used by generated code |
220
+ | `@ignex/nova/package.json` | `package.json` — version / metadata for tooling |
164
221
 
165
222
  **Native addon:** the tarball ships `rust/` source + `prebuilds/<platform>-<arch>/`
166
223
  for the platforms built at release (see CI). If your platform has a prebuild it
167
224
  just works. Otherwise either rebuild from the shipped source:
168
225
 
169
226
  ```bash
170
- cargo build --release --manifest-path node_modules/ignex-nova/rust/Cargo.toml
227
+ cargo build --release --manifest-path node_modules/@ignex/nova/rust/Cargo.toml
171
228
  # loader finds it at <pkg>/rust/target/release/…
172
229
  # or point at any build explicitly:
173
230
  IGNEX_FFI_PATH=/abs/path/to/libignex_ffi.so bun run your-server.ts
@@ -180,9 +237,11 @@ staged, and published to npm.
180
237
 
181
238
  | Path | Role |
182
239
  | --- | --- |
183
- | `src/schema/index.ts` | TypeBox schemas + `events`/`controlEvents` registries (source of truth) |
184
- | `scripts/` | `generate.ts` orchestrator + `.fbs` / Rust-glue / registry / ts-ser emitters |
185
- | `src/generated/` | flatc `--ts`/`--rust` output + `registry.ts` + `direct-ser.ts` + `ts-ser.ts` |
240
+ | `src/schema/index.ts` | built-in TypeBox schemas + `events`/`controlEvents` registries (source of truth for the DEFAULT registry) |
241
+ | `src/bindings/` | the generic `Bindings` contract: `types.ts` (runtime wire-stack interface + `EventNameOf`/`EventsOf`), `assemble.ts`, `default.ts` (built-in bindings) |
242
+ | `src/codegen/` | the schema→wire emitters (published — `@ignex/nova/generate` uses them at runtime) |
243
+ | `scripts/` | dev tooling: `generate.ts` orchestrator, prebuild/pack/release helpers (not published) |
244
+ | `src/generated/` | flatc `--ts`/`--rust` output + `registry.ts` + `direct-ser.ts` + `ts-ser.ts` (built-in schema) |
186
245
  | `rust/` | cdylib: `ffi.rs` (C-ABI), `transcode/generated.rs` (glue) |
187
246
  | `src/native/` | `bun:ffi` binding, self-tests, per-platform addon loader |
188
247
  | `src/transport/` | `transport.ts` (`encodeToScratch`), `scratch.ts` (reusable zero-alloc buffer), `stats.ts` |
@@ -190,21 +249,25 @@ staged, and published to npm.
190
249
  | `src/bridge/` | optional NATS bridge: `nats.ts` (injectable transport, eager non-blocking connect), `subjects.ts` (subject naming) |
191
250
  | `public/server.ts` | entrypoint shim: `createServer` (publish/rooms/groups/targeting/NATS/auth/backpressure/metrics/drain) |
192
251
  | `public/client.ts` | entrypoint shim: `createClient` (on/send/subscribe/joinGroup/reconnect/heartbeat/status) |
193
- | `public/nats.ts` | entrypoint shim: `createNatsBridge` standalone (`ignex-nova/nats`) |
252
+ | `public/nats.ts` | entrypoint shim: `createNatsBridge` standalone (`@ignex/nova/nats`) |
194
253
  | `client/` | browser demo (built to `client-dist/`) |
195
254
  | `bench/` | serialize latency + end-to-end throughput (+ `BASELINE.md` perf gate) |
196
255
  | `examples/` | `nats-consumer.ts` — independent NATS consumer for bridged frames |
197
256
  | `prebuilds/` | staged native addons per platform (`<platform>-<arch>/`), built by `bun run prebuild` / CI |
198
- | `docs/` | `wire-format.md`, `architecture.md`, `publishing.md` |
257
+ | `docs/` | `wire-format.md`, `architecture.md`, `publishing.md`, `events.md`, `generic-bindings.md` |
199
258
 
200
259
  ## Adding an event
201
260
 
202
- 1. Define the payload in `schema/index.ts` (TypeBox), add it to `schemas`
203
- (if it's a named table) and to `events` (name → schema). For exact integer
204
- values use `Type.BigInt()`.
205
- 2. Re-run `bun run generate`, `cargo build --release`, `bun run build:client`.
206
- 3. `server.publish("yourEvent", payload)`, `client.on("yourEvent", cb)`, and
207
- `client.send("yourEvent", payload)` are now fully typed on both sides.
261
+ **Built-in registry:** 1. Define the payload in `schema/index.ts` (TypeBox), add
262
+ it to `schemas` (if it's a named table) and to `events` (name → schema). For
263
+ exact integer values use `Type.BigInt()`. 2. Re-run `bun run generate`,
264
+ `cargo build --release`, `bun run build:client`. 3.
265
+ `server.publish("yourEvent", payload)`, `client.on("yourEvent", cb)`, and
266
+ `client.send("yourEvent", payload)` are now fully typed on both sides.
267
+
268
+ **Your own registry:** you don't edit this repo at all — define the schema in
269
+ your app and run `generateBindings` (see
270
+ [docs/generic-bindings.md](docs/generic-bindings.md)).
208
271
 
209
272
  ## Performance (min-of-N ns/op on this machine, `bun run bench:serialize`)
210
273
 
@@ -216,7 +279,7 @@ staged, and published to npm.
216
279
 
217
280
  Directable events serialize with **~0 B/op** (reusable scratch, no per-call
218
281
  allocations, no JSON). End-to-end over a real WebSocket (`bench:throughput`):
219
- **~1.18M msg/s**.
282
+ **~1.09M msg/s** — see `bench/BASELINE.md` for the perf gate.
220
283
 
221
284
  ## Server options (all optional)
222
285
 
@@ -275,6 +338,43 @@ client.onStatus(() => console.log("my id:", client.clientId));
275
338
  NATS consumers (any language) decode bridged frames from
276
339
  `src/generated/fbs/backend.fbs` + the FNV-1a id map in `src/generated/wire-registry.json`
277
340
  — see [docs/wire-format.md](docs/wire-format.md) and `examples/nats-consumer.ts`.
341
+ For your OWN schema, `generateBindings` emits the same `backend.fbs` +
342
+ `wire-registry.json` into your project — see
343
+ [docs/generic-bindings.md](docs/generic-bindings.md).
344
+
345
+ ## Events layer (the event-driven system)
346
+
347
+ ```ts
348
+ import { createServer } from "@ignex/nova/server";
349
+ import { on, emit, emitToUser } from "@ignex/nova/events"; // global singleton
350
+
351
+ const server = createServer({
352
+ port: 3000,
353
+ events: {
354
+ onConnect: (client) => client.data.set("since", client.connectedAt),
355
+ cluster: { nats: true }, // optional: cross-instance sync
356
+ },
357
+ });
358
+
359
+ // the events file — receive events (ctx carries the sender's client record)
360
+ on("chat.message", (payload, ctx) => {
361
+ emitToUser(payload.to, "chat.delivered", { id: payload.id });
362
+ });
363
+
364
+ // the global emit — send events through websockets from anywhere
365
+ emit("quote", { symbol: "AAPL", bid: 180.1, ask: 180.2 }); // broadcast
366
+ emitToUser("u-42", "order.update", { orderId: "o-1" }); // a user's sockets
367
+ emit("alert", { text: "halt" }, { type: "group", group: "traders" }); // to a group
368
+
369
+ // client records: id / userId (on whose behalf) / data / groups
370
+ server.events.client("c-1")?.data.set("tier", "gold");
371
+ server.events.clientsByUser("u-42"); // every device of u-42
372
+ server.events.userGroup("ops").add("u-42").emit("pager", { text: "…" });
373
+ ```
374
+
375
+ Full API, cluster semantics, presence, shared-state (Redis) indexes and
376
+ performance notes: **[docs/events.md](docs/events.md)**. The events layer is
377
+ opt-in — without `events`, there is zero overhead.
278
378
 
279
379
  ## Publishing to npm
280
380
 
@@ -283,7 +383,7 @@ Publish directly from source — `bun publish` runs the release gate
283
383
 
284
384
  ```bash
285
385
  bun run release:dry # plan only — print what would happen
286
- bun run release # patch bump → verify → publish → commit/tag/push
386
+ bun run release # patch bump → verify → publish → commit + tag (push only with `--push`)
287
387
  bun run release minor # minor bump
288
388
  bun run release --version 0.2.0 # explicit version
289
389
  ```
@@ -0,0 +1,81 @@
1
+ # Local Development with the Core Projects — `bun link`
2
+
3
+ > **Scope**: maintainers and AI agents working across the IgnEX core stack.
4
+ > Application developers consume published versions from npm and do **not**
5
+ > need this file.
6
+
7
+ ## Why
8
+
9
+ The IgnEX core packages live side-by-side, one directory back from this repo,
10
+ in `/home/adeel/poc/`. When a change in this repo must be tested against the
11
+ local source of a core project (or a core change must be tested against this
12
+ repo), use `bun link` so the consumer resolves the local directory instead of
13
+ the npm registry. This is the supported Bun ≥ 1.4 (Rust-based runtime)
14
+ local-development mechanism
15
+ ([`bun link` docs](https://bun.com/docs/cli/link),
16
+ [bun.com/blog/bun-v1.4](https://bun.com/blog/bun-v1.4)).
17
+
18
+ This workflow is **only for maintainers and AI agents working with the core
19
+ projects**. CI and release pipelines always resolve from the registry; local
20
+ links are a development-only convenience.
21
+
22
+ ## Core packages (one directory back)
23
+
24
+ | Repo (`/home/adeel/poc/`) | Package(s) | `bun link` name |
25
+ | --- | --- | --- |
26
+ | `ignus` — ignex monorepo, workspaces `packages/*` | `@ignex/core`, `@ignex/cli`, `@ignex/compiler`, `@ignex/native`, `@ignex/shared`, `@ignex/mcp`, `@ignex/app`, `create-ignex` | run `bun link` inside each package dir |
27
+ | `bun-rust-runtime-bench` | `castrum` (Rust addon `castrum.<platform>-<arch>.node`) | `castrum` |
28
+ | `ignex-mongodb` | `@ignex/ninox` | `@ignex/ninox` |
29
+ | `ignex-nova` | `@ignex/nova` | `@ignex/nova` |
30
+
31
+ Known cross-repo edges (verify with `grep` in `package.json` before assuming):
32
+
33
+ - `@ignex/native` (in `ignus/packages/native`) depends on `castrum` — link
34
+ `castrum` there when changing the addon wire (`createNativeRoute`).
35
+ - `@ignex/core` (in `ignus/packages/core`) optionally depends on
36
+ `@ignex/nova`; `@ignex/app` depends on `@ignex/core`, `@ignex/cli`, and
37
+ `@ignex/ninox`.
38
+
39
+ ## How to link
40
+
41
+ ```bash
42
+ # 1. Register the core package (once per machine, from the core repo):
43
+ cd /home/adeel/poc/ignex-mongodb
44
+ bun link # → Success! Registered "@ignex/ninox"
45
+
46
+ # 2. Link it into the consumer project:
47
+ cd /home/adeel/poc/ignex-app
48
+ bun link @ignex/ninox # symlinks node_modules/@ignex/ninox → ../ignex-mongodb
49
+ bun link @ignex/ninox --save # also writes "link:@ignex/ninox" into package.json deps
50
+ ```
51
+
52
+ - `bun link` (no args) in a package directory registers that package globally
53
+ for this user.
54
+ - `bun link <name>` in a consumer creates a symlink in the consumer's
55
+ `node_modules` pointing at the registered directory. `--save` additionally
56
+ records `"<name>": "link:<name>"` in `package.json` dependencies.
57
+ - Unregister a package: `bun unlink` from the core repo dir.
58
+ - Return to registry versions: remove the `link:` entry (or `bun unlink
59
+ <name>` in the consumer) and `bun install`.
60
+
61
+ ## Rust-core caveats (castrum & @ignex/nova)
62
+
63
+ Both `castrum` and `@ignex/nova` ship a Rust cdylib. After changing Rust
64
+ source, rebuild the addon **before** linking or using:
65
+
66
+ - `castrum`: `bun run build` (release `napi build`) or `bun run build:debug`.
67
+ Under Bun the addon is also called through `bun:ffi` from the same cdylib.
68
+ - `@ignex/nova`: `bun run build:rust`
69
+ (`cargo build --release --manifest-path rust/Cargo.toml`), or rely on
70
+ `prepack`/`prebuild` to stage `prebuilds/<platform>-<arch>/`.
71
+
72
+ A stale `.node`/`.so` silently serves old behavior — rebuild, then re-test.
73
+ `@ignex/nova` additionally fails its bind-time self-test on schema/wire-version
74
+ mismatch (`IGNEX_FFI_PATH` can point at a specific build); `castrum` falls back
75
+ to the napi transport when the `bun:ffi` self-test fails.
76
+
77
+ ## Never publish from a linked tree
78
+
79
+ Publishing a consumer whose dependencies are `link:` entries ships symlinks,
80
+ not packages. Releases always run against registry versions (`prepublishOnly` /
81
+ CI re-verify with a clean install). Keep `bun link` strictly local.
@@ -0,0 +1,232 @@
1
+ # TREE — generated scaffolding map
2
+
3
+ > AUTO-GENERATED by `bun run gen:ai-map` (`scripts/gen-ai-map.ts`).
4
+ > Do not edit by hand — regenerate after structural changes. The
5
+ > curated maps live in `docs/architecture.md` + `docs/wire-format.md`
6
+ > (and `AGENTS.md` for agents).
7
+
8
+ - package: `@ignex/nova` v0.1.1
9
+ - engines: {"bun":">=1.4"}
10
+ - rust crate: `ignex-nova-ffi` v0.1.0
11
+ - scripts (19): `generate`, `build:rust`, `build:client`, `build:dist`, `build`, `prebuild`, `test`, `lint`, `typecheck`, `verify`, `pack:check`, `gen:ai-map`, …
12
+
13
+ ## src/
14
+
15
+ ```
16
+ src/
17
+ ├─ bindings/
18
+ │ ├─ assemble.ts
19
+ │ ├─ default.ts
20
+ │ └─ types.ts
21
+ ├─ bridge/
22
+ │ ├─ nats.ts
23
+ │ └─ subjects.ts
24
+ ├─ codegen/
25
+ │ ├─ constants.ts
26
+ │ ├─ direct-gen.ts
27
+ │ ├─ fingerprint.ts
28
+ │ ├─ hash.ts
29
+ │ ├─ registry-gen.ts
30
+ │ ├─ rust-glue-gen.ts
31
+ │ ├─ schema-model.ts
32
+ │ ├─ ts-ser-gen.ts
33
+ │ └─ typebox-to-fbs.ts
34
+ ├─ core/
35
+ │ ├─ auth.ts
36
+ │ ├─ backpressure.ts
37
+ │ ├─ client-heartbeat.ts
38
+ │ ├─ client-reconnect.ts
39
+ │ ├─ client-state.ts
40
+ │ ├─ client-wire.ts
41
+ │ ├─ client.ts
42
+ │ ├─ groups.ts
43
+ │ ├─ int64-guard.ts
44
+ │ ├─ metrics.ts
45
+ │ ├─ outbound.ts
46
+ │ ├─ replay.ts
47
+ │ ├─ ring.ts
48
+ │ ├─ rooms.ts
49
+ │ ├─ routing.ts
50
+ │ ├─ server.ts
51
+ │ └─ state.ts
52
+ ├─ events/
53
+ │ ├─ clients.ts
54
+ │ ├─ cluster.ts
55
+ │ ├─ data.ts
56
+ │ ├─ emit.ts
57
+ │ ├─ global.ts
58
+ │ ├─ groups.ts
59
+ │ ├─ hub.ts
60
+ │ ├─ index.ts
61
+ │ ├─ queue.ts
62
+ │ ├─ registry.ts
63
+ │ └─ types.ts
64
+ ├─ generated/
65
+ │ ├─ fbs/
66
+ │ │ └─ backend.fbs
67
+ │ ├─ rust/
68
+ │ │ └─ backend_generated.rs
69
+ │ ├─ ts/
70
+ │ │ ├─ backend.ts
71
+ │ │ ├─ big-val.ts
72
+ │ │ ├─ complex.ts
73
+ │ │ ├─ customer.ts
74
+ │ │ ├─ hello.ts
75
+ │ │ ├─ join-group.ts
76
+ │ │ ├─ leave-group.ts
77
+ │ │ ├─ order-billing.ts
78
+ │ │ ├─ order-line.ts
79
+ │ │ ├─ order.ts
80
+ │ │ ├─ ping.ts
81
+ │ │ ├─ pong.ts
82
+ │ │ ├─ portfolio-position.ts
83
+ │ │ ├─ portfolio-snapshot.ts
84
+ │ │ ├─ quote.ts
85
+ │ │ ├─ side.ts
86
+ │ │ ├─ snapshot-request.ts
87
+ │ │ ├─ subscribe.ts
88
+ │ │ ├─ tags.ts
89
+ │ │ ├─ trade.ts
90
+ │ │ ├─ unsubscribe.ts
91
+ │ │ └─ welcome.ts
92
+ │ ├─ direct-ser.ts
93
+ │ ├─ registry.ts
94
+ │ ├─ ts-ser.ts
95
+ │ └─ wire-registry.json
96
+ ├─ native/
97
+ │ ├─ codec.ts
98
+ │ ├─ ffi.ts
99
+ │ └─ loader.ts
100
+ ├─ schema/
101
+ │ └─ index.ts
102
+ ├─ transport/
103
+ │ ├─ byte-buffer-pool.ts
104
+ │ ├─ scratch.ts
105
+ │ ├─ stats.ts
106
+ │ └─ transport.ts
107
+ └─ server.ts
108
+ ```
109
+
110
+ ## rust/
111
+
112
+ ```
113
+ rust/
114
+ ├─ examples/
115
+ │ └─ dump.rs
116
+ ├─ src/
117
+ │ ├─ generated/
118
+ │ │ ├─ backend.rs
119
+ │ │ └─ mod.rs
120
+ │ ├─ transcode/
121
+ │ │ ├─ generated.rs
122
+ │ │ └─ mod.rs
123
+ │ ├─ ffi.rs
124
+ │ └─ lib.rs
125
+ ├─ .npmignore
126
+ ├─ Cargo.lock
127
+ └─ Cargo.toml
128
+ ```
129
+
130
+ ## public/
131
+
132
+ ```
133
+ public/
134
+ ├─ bindings.ts
135
+ ├─ client.ts
136
+ ├─ events.ts
137
+ ├─ generate.ts
138
+ ├─ internal.ts
139
+ ├─ nats.ts
140
+ └─ server.ts
141
+ ```
142
+
143
+ ## client/
144
+
145
+ ```
146
+ client/
147
+ ├─ index.html
148
+ └─ main.ts
149
+ ```
150
+
151
+ ## test/
152
+
153
+ ```
154
+ test/
155
+ ├─ api.test.ts
156
+ ├─ auth.test.ts
157
+ ├─ backpressure.test.ts
158
+ ├─ bidirectional.test.ts
159
+ ├─ bindings-gen.test.ts
160
+ ├─ byte-buffer-pool.test.ts
161
+ ├─ direct.test.ts
162
+ ├─ e2e.test.ts
163
+ ├─ events-cluster.test.ts
164
+ ├─ events.test.ts
165
+ ├─ ffi.test.ts
166
+ ├─ groups.test.ts
167
+ ├─ helpers.ts
168
+ ├─ int64.test.ts
169
+ ├─ integrity.test.ts
170
+ ├─ loader.test.ts
171
+ ├─ metrics.test.ts
172
+ ├─ nats-bridge.test.ts
173
+ ├─ nats-integration.test.ts
174
+ ├─ reconnect.test.ts
175
+ ├─ ring.test.ts
176
+ ├─ rooms.test.ts
177
+ ├─ roundtrip.test.ts
178
+ ├─ security.test.ts
179
+ ├─ targeting.test.ts
180
+ └─ wire.test.ts
181
+ ```
182
+
183
+ ## bench/
184
+
185
+ ```
186
+ bench/
187
+ ├─ BASELINE.md
188
+ ├─ ffi-margin.ts
189
+ ├─ measure.ts
190
+ ├─ serialize.ts
191
+ └─ throughput.ts
192
+ ```
193
+
194
+ ## scripts/
195
+
196
+ ```
197
+ scripts/
198
+ ├─ build-prebuild.ts
199
+ ├─ check-pack.ts
200
+ ├─ gen-ai-map.ts
201
+ ├─ generate.ts
202
+ └─ release.ts
203
+ ```
204
+
205
+ ## examples/
206
+
207
+ ```
208
+ examples/
209
+ ├─ consumer.ts
210
+ ├─ events.ts
211
+ ├─ generic-schema.ts
212
+ └─ nats-consumer.ts
213
+ ```
214
+
215
+ ## docs/
216
+
217
+ ```
218
+ docs/
219
+ ├─ ai/
220
+ │ ├─ LOCAL_DEV.md
221
+ │ └─ TREE.md
222
+ ├─ architecture.md
223
+ ├─ events.md
224
+ ├─ generic-bindings.md
225
+ ├─ publishing.md
226
+ └─ wire-format.md
227
+ ```
228
+
229
+ ## Notes
230
+
231
+ - `SKIP` set in the script excludes build output (target/, dist/, client-dist/, prebuilds/, node_modules/).
232
+ - `src/generated/`, `rust/src/generated/`, `rust/src/transcode/generated.rs` are regenerated by `bun run generate` — never hand-edit.
@@ -5,25 +5,37 @@ a Rust FFI serializer — hidden behind a typed pub/sub API.
5
5
 
6
6
  ## Pipeline
7
7
 
8
+ The wire stack is generated from TypeBox schemas. The built-in registry
9
+ (`src/schema/index.ts`) runs the in-repo orchestrator (`scripts/generate.ts`);
10
+ **any** app can run the same emitters on its own schema via
11
+ `generateBindings` (`@ignex/nova/generate` → `src/codegen/`).
12
+
8
13
  ```
9
14
  src/schema/index.ts (TypeBox — single source of truth: app events + control events)
10
- │ scripts/generate.ts
15
+ │ scripts/generate.ts (or generateBindings() for YOUR schema)
11
16
 
12
17
  backend.fbs ──flatc --ts──▶ src/generated/ts/ (decoders, both sides)
13
18
  └───flatc --rust──▶ rust/src/generated/ (table builders)
14
- scripts/rust-glue-gen.ts ─▶ rust/src/transcode/ (JSON glue + direct-args FFI)
15
- scripts/direct-gen.ts ────▶ src/generated/direct-ser.ts (zero-alloc server encoder)
16
- scripts/ts-ser-gen.ts ─────▶ src/generated/ts-ser.ts (pure-JS browser encoder)
17
- scripts/registry-gen.ts ───▶ src/generated/registry.ts (event routing, both sides)
18
- scripts/generate.ts ────────▶ src/generated/wire-registry.json (name→id map for external consumers)
19
+ src/codegen/rust-glue-gen.ts ─▶ rust/src/transcode/ (JSON glue + direct-args FFI)
20
+ src/codegen/direct-gen.ts ────▶ src/generated/direct-ser.ts (zero-alloc server encoder)
21
+ src/codegen/ts-ser-gen.ts ─────▶ src/generated/ts-ser.ts (pure-JS browser encoder)
22
+ src/codegen/registry-gen.ts ───▶ src/generated/registry.ts (event routing, both sides)
23
+ scripts/generate.ts ───────────▶ src/generated/wire-registry.json (name→id map for external consumers)
19
24
  ```
20
25
 
26
+ Every artifact is also emitted per-user-schema by `generateBindings`
27
+ (`src/codegen/*` emitters, `public/generate.ts`) into the app's
28
+ `ignex/generated/` folder, then assembled into a runtime `Bindings`
29
+ (`src/bindings/`) that `createServer` / `createClient` / `createNatsBridge`
30
+ accept via `options.bindings` (default: the built-in `defaultBindings`). See
31
+ [docs/generic-bindings.md](generic-bindings.md).
32
+
21
33
  ## Functional composition
22
34
 
23
35
  The public API is built by **functional composition over an explicit state
24
36
  object** — no classes, no `this`. `public/server.ts` and `public/client.ts`
25
- are thin re-export shims that keep the npm entrypoints (`ignex-nova/server`,
26
- `ignex-nova/client`) and the `dist` build stable; the implementation lives in
37
+ are thin re-export shims that keep the npm entrypoints (`@ignex/nova/server`,
38
+ `@ignex/nova/client`) and the `dist` build stable; the implementation lives in
27
39
  `src/core/`.
28
40
 
29
41
  - **Composition roots** (`src/core/server.ts`, `src/core/client.ts`) — the only
@@ -68,7 +80,7 @@ frames) are encoded by `generated/ts-ser.ts` — flatc's object API (`XxxT` +
68
80
  ## Runtime layout
69
81
 
70
82
  - `public/server.ts`, `public/client.ts`, `public/nats.ts` — thin re-export shims
71
- (npm entrypoints `ignex-nova/server` / `client` / `nats` + the `dist` build).
83
+ (npm entrypoints `@ignex/nova/server` / `client` / `nats` + the `dist` build).
72
84
  Implementation is in `src/core/` + `src/bridge/`.
73
85
  - `src/core/server.ts` — `createServer` composition root: `Bun.serve`, client
74
86
  registry (id → socket), rooms, groups, inbound routing, control frames,
@@ -88,9 +100,22 @@ frames) are encoded by `generated/ts-ser.ts` — flatc's object API (`XxxT` +
88
100
  subject builders (`ignex.broadcast.*` / `ignex.topic.*` / `ignex.group.*` /
89
101
  inbound `ignex.inbound.>`). Outbound frames are copied from the shared
90
102
  scratch; inbound frames are decoded via `readFrameHeader`/`decodePayload` and
91
- forwarded to clients (never re-bridged).
103
+ forwarded to clients (never re-bridged). `subscribeRaw` exposes raw byte
104
+ subscriptions (re-subscribed on reconnect) for the events cluster layer.
92
105
  - `src/core/metrics.ts`, `src/core/int64-guard.ts` — `createMetrics()` factory
93
106
  + the exact-int64 safety net.
107
+ - `src/events/*` — the events layer (opt-in via `createServer({ events })`,
108
+ public entry `@ignex/nova/events` → `public/events.ts`): `hub.ts` composition
109
+ root (`server.events`, binds the module-global `emit`/`on` singleton),
110
+ `types.ts` (`EventClient` records with `userId` + per-connection `data`,
111
+ `EmitTarget` discriminated union, hub/options interfaces), `registry.ts`
112
+ (multi-handler dispatch with isolation), `clients.ts`/`data.ts` (client
113
+ store: byId + byUser index), `groups.ts` (client groups reusing the
114
+ transport registry + user groups), `emit.ts` (encode-once local fan-out +
115
+ bridge + cluster), `cluster.ts` (origin-tagged envelope, NATS/Redis/custom
116
+ transports, presence, shared-state indexes), `queue.ts` (bounded offload
117
+ workers that keep all cluster/state work off the WS hot path), `global.ts`
118
+ (the importable `emit`/`emitToGroup`/… singleton).
94
119
  - `src/transport/{transport,scratch,stats}.ts` — object → frame encoding:
95
120
  `encodeToScratch` (direct/JSON), the reusable zero-alloc scratch, and
96
121
  encode-path stats.