@ignex/nova 0.1.1 → 0.1.5

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 (120) hide show
  1. package/README.md +136 -33
  2. package/docs/ai/LOCAL_DEV.md +81 -0
  3. package/docs/ai/TREE.md +292 -0
  4. package/docs/architecture.md +101 -28
  5. package/docs/events.md +252 -0
  6. package/docs/generic-bindings.md +207 -0
  7. package/docs/publishing.md +2 -2
  8. package/docs/wire-format.md +74 -20
  9. package/index.ts +75 -27
  10. package/package.json +13 -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 +510 -0
  16. package/public/internal.ts +16 -0
  17. package/public/nats.ts +9 -5
  18. package/public/server.ts +52 -16
  19. package/rust/src/ffi.rs +10 -0
  20. package/rust/src/generated/backend.rs +503 -0
  21. package/rust/src/transcode/generated.rs +377 -17
  22. package/src/bindings/assemble.ts +73 -0
  23. package/src/bindings/default.ts +65 -0
  24. package/src/bindings/types.ts +113 -0
  25. package/src/bridge/nats/inbound.ts +46 -0
  26. package/src/bridge/nats/index.ts +131 -0
  27. package/src/bridge/nats/real-transport.ts +133 -0
  28. package/src/bridge/nats/types.ts +80 -0
  29. package/src/bridge/subjects.ts +3 -0
  30. package/src/codegen/constants.ts +28 -0
  31. package/src/codegen/direct-gen.ts +564 -0
  32. package/src/codegen/fingerprint.ts +44 -0
  33. package/src/codegen/hash.ts +25 -0
  34. package/src/codegen/registry-gen.ts +246 -0
  35. package/src/codegen/rust-glue-gen.ts +552 -0
  36. package/src/codegen/schema-model.ts +363 -0
  37. package/src/codegen/ts-ser-gen.ts +230 -0
  38. package/src/codegen/typebox-to-fbs.ts +60 -0
  39. package/src/core/auth.ts +67 -5
  40. package/src/core/client-heartbeat.ts +2 -1
  41. package/src/core/client-reconnect.ts +9 -2
  42. package/src/core/client-rpc.ts +75 -0
  43. package/src/core/client-state.ts +63 -8
  44. package/src/core/client-wire.ts +148 -15
  45. package/src/core/client.ts +105 -31
  46. package/src/core/groups.ts +8 -0
  47. package/src/core/metrics.ts +42 -21
  48. package/src/core/outbound.ts +62 -11
  49. package/src/core/rate-limit.ts +69 -0
  50. package/src/core/replay.ts +41 -1
  51. package/src/core/resume.ts +181 -0
  52. package/src/core/rooms.ts +10 -3
  53. package/src/core/routing.ts +144 -12
  54. package/src/core/server/client-info.ts +37 -0
  55. package/src/core/server/http-routes.ts +59 -0
  56. package/src/core/server/index.ts +360 -0
  57. package/src/core/server/metrics-view.ts +53 -0
  58. package/src/core/server/socket-lifecycle.ts +57 -0
  59. package/src/core/state.ts +124 -14
  60. package/src/core/topic-log.ts +86 -0
  61. package/src/events/clients.ts +174 -0
  62. package/src/events/cluster/dedupe.ts +43 -0
  63. package/src/events/cluster/envelope.ts +149 -0
  64. package/src/events/cluster/index.ts +50 -0
  65. package/src/events/cluster/keys.ts +33 -0
  66. package/src/events/cluster/kinds.ts +32 -0
  67. package/src/events/cluster/presence-table.ts +99 -0
  68. package/src/events/cluster/presence.ts +53 -0
  69. package/src/events/cluster/redis-client.ts +50 -0
  70. package/src/events/cluster/store-memory.ts +67 -0
  71. package/src/events/cluster/store-redis.ts +44 -0
  72. package/src/events/cluster/subjects.ts +30 -0
  73. package/src/events/cluster/sync.ts +476 -0
  74. package/src/events/cluster/transport-nats.ts +24 -0
  75. package/src/events/cluster/transport-redis.ts +120 -0
  76. package/src/events/cluster-rpc.ts +196 -0
  77. package/src/events/data.ts +38 -0
  78. package/src/events/delivery.ts +83 -0
  79. package/src/events/emit.ts +173 -0
  80. package/src/events/global.ts +117 -0
  81. package/src/events/groups.ts +118 -0
  82. package/src/events/hub/context-factory.ts +79 -0
  83. package/src/events/hub/dispatch.ts +86 -0
  84. package/src/events/hub/index.ts +536 -0
  85. package/src/events/hub/internal.ts +31 -0
  86. package/src/events/hub/metrics-snapshot.ts +84 -0
  87. package/src/events/hub/resolve-cluster.ts +49 -0
  88. package/src/events/index.ts +61 -0
  89. package/src/events/queue.ts +123 -0
  90. package/src/events/registry.ts +214 -0
  91. package/src/events/schedule.ts +73 -0
  92. package/src/events/trace.ts +283 -0
  93. package/src/events/types/client.ts +68 -0
  94. package/src/events/types/cluster.ts +40 -0
  95. package/src/events/types/context.ts +50 -0
  96. package/src/events/types/emit-target.ts +29 -0
  97. package/src/events/types/groups.ts +35 -0
  98. package/src/events/types/hub.ts +124 -0
  99. package/src/events/types/index.ts +30 -0
  100. package/src/events/types/metrics.ts +52 -0
  101. package/src/events/types/options.ts +62 -0
  102. package/src/generated/direct-ser.ts +148 -60
  103. package/src/generated/fbs/backend.fbs +24 -1
  104. package/src/generated/registry.ts +94 -33
  105. package/src/generated/rust/backend_generated.rs +503 -0
  106. package/src/generated/ts/backend.ts +4 -0
  107. package/src/generated/ts/resume.ts +74 -0
  108. package/src/generated/ts/resumed.ts +88 -0
  109. package/src/generated/ts/rpc-call.ts +112 -0
  110. package/src/generated/ts/rpc-result.ts +126 -0
  111. package/src/generated/ts/snapshot-request.ts +19 -5
  112. package/src/generated/ts-ser.ts +110 -17
  113. package/src/generated/wire-registry.json +7 -2
  114. package/src/native/ffi.ts +85 -28
  115. package/src/schema/index.ts +49 -2
  116. package/src/server.ts +7 -3
  117. package/src/transport/transport.ts +200 -79
  118. package/src/bridge/nats.ts +0 -269
  119. package/src/core/server.ts +0 -294
  120. package/src/transport/stats.ts +0 -44
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
 
@@ -229,9 +292,12 @@ createServer({
229
292
  replay: { historySize: 64 }, // per-topic last-value replay on subscribe
230
293
  authenticate: async (req) => checkToken(req.headers.get("authorization")),
231
294
  allowedOrigins: ["http://localhost:3000"],
232
- token: "shared-secret", // or (tok) => boolean
295
+ token: "shared-secret", // or (tok) => boolean — constant-time compared
233
296
  maxConnections: 10_000,
234
297
  maxMessageSize: 64 * 1024,
298
+ rateLimit: { messagesPerSecond: 100, burst: 200, policy: "drop" }, // per-conn inbound bucket ("close" → 1008)
299
+ authorizeTopic: (topic, ws) => topic !== "admin", // gate room joins (all paths)
300
+ authorizeGroup: (group, ws) => ws.data.userId != null, // gate group joins
235
301
  int64Guard: "warn", // "off" | "throw" | "warn"
236
302
  nats: { servers: ["nats://localhost:4222"], inbound: true }, // optional NATS bridge
237
303
  tls: { keyFile, certFile }, // enables wss://
@@ -275,6 +341,43 @@ client.onStatus(() => console.log("my id:", client.clientId));
275
341
  NATS consumers (any language) decode bridged frames from
276
342
  `src/generated/fbs/backend.fbs` + the FNV-1a id map in `src/generated/wire-registry.json`
277
343
  — see [docs/wire-format.md](docs/wire-format.md) and `examples/nats-consumer.ts`.
344
+ For your OWN schema, `generateBindings` emits the same `backend.fbs` +
345
+ `wire-registry.json` into your project — see
346
+ [docs/generic-bindings.md](docs/generic-bindings.md).
347
+
348
+ ## Events layer (the event-driven system)
349
+
350
+ ```ts
351
+ import { createServer } from "@ignex/nova/server";
352
+ import { on, emit, emitToUser } from "@ignex/nova/events"; // global singleton
353
+
354
+ const server = createServer({
355
+ port: 3000,
356
+ events: {
357
+ onConnect: (client) => client.data.set("since", client.connectedAt),
358
+ cluster: { nats: true }, // optional: cross-instance sync
359
+ },
360
+ });
361
+
362
+ // the events file — receive events (ctx carries the sender's client record)
363
+ on("chat.message", (payload, ctx) => {
364
+ emitToUser(payload.to, "chat.delivered", { id: payload.id });
365
+ });
366
+
367
+ // the global emit — send events through websockets from anywhere
368
+ emit("quote", { symbol: "AAPL", bid: 180.1, ask: 180.2 }); // broadcast
369
+ emitToUser("u-42", "order.update", { orderId: "o-1" }); // a user's sockets
370
+ emit("alert", { text: "halt" }, { type: "group", group: "traders" }); // to a group
371
+
372
+ // client records: id / userId (on whose behalf) / data / groups
373
+ server.events.client("c-1")?.data.set("tier", "gold");
374
+ server.events.clientsByUser("u-42"); // every device of u-42
375
+ server.events.userGroup("ops").add("u-42").emit("pager", { text: "…" });
376
+ ```
377
+
378
+ Full API, cluster semantics, presence, shared-state (Redis) indexes and
379
+ performance notes: **[docs/events.md](docs/events.md)**. The events layer is
380
+ opt-in — without `events`, there is zero overhead.
278
381
 
279
382
  ## Publishing to npm
280
383
 
@@ -283,7 +386,7 @@ Publish directly from source — `bun publish` runs the release gate
283
386
 
284
387
  ```bash
285
388
  bun run release:dry # plan only — print what would happen
286
- bun run release # patch bump → verify → publish → commit/tag/push
389
+ bun run release # patch bump → verify → publish → commit + tag (push only with `--push`)
287
390
  bun run release minor # minor bump
288
391
  bun run release --version 0.2.0 # explicit version
289
392
  ```
@@ -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,292 @@
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.3
9
+ - engines: {"bun":">=1.4"}
10
+ - rust crate: `ignex-nova-ffi` v0.1.0
11
+ - scripts (20): `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/
23
+ │ │ ├─ inbound.ts
24
+ │ │ ├─ index.ts
25
+ │ │ ├─ real-transport.ts
26
+ │ │ └─ types.ts
27
+ │ └─ subjects.ts
28
+ ├─ codegen/
29
+ │ ├─ constants.ts
30
+ │ ├─ direct-gen.ts
31
+ │ ├─ fingerprint.ts
32
+ │ ├─ hash.ts
33
+ │ ├─ registry-gen.ts
34
+ │ ├─ rust-glue-gen.ts
35
+ │ ├─ schema-model.ts
36
+ │ ├─ ts-ser-gen.ts
37
+ │ └─ typebox-to-fbs.ts
38
+ ├─ core/
39
+ │ ├─ server/
40
+ │ │ ├─ client-info.ts
41
+ │ │ ├─ http-routes.ts
42
+ │ │ ├─ index.ts
43
+ │ │ ├─ metrics-view.ts
44
+ │ │ └─ socket-lifecycle.ts
45
+ │ ├─ auth.ts
46
+ │ ├─ backpressure.ts
47
+ │ ├─ client-heartbeat.ts
48
+ │ ├─ client-reconnect.ts
49
+ │ ├─ client-rpc.ts
50
+ │ ├─ client-state.ts
51
+ │ ├─ client-wire.ts
52
+ │ ├─ client.ts
53
+ │ ├─ groups.ts
54
+ │ ├─ int64-guard.ts
55
+ │ ├─ metrics.ts
56
+ │ ├─ outbound.ts
57
+ │ ├─ rate-limit.ts
58
+ │ ├─ replay.ts
59
+ │ ├─ resume.ts
60
+ │ ├─ ring.ts
61
+ │ ├─ rooms.ts
62
+ │ ├─ routing.ts
63
+ │ ├─ state.ts
64
+ │ └─ topic-log.ts
65
+ ├─ events/
66
+ │ ├─ cluster/
67
+ │ │ ├─ dedupe.ts
68
+ │ │ ├─ envelope.ts
69
+ │ │ ├─ index.ts
70
+ │ │ ├─ keys.ts
71
+ │ │ ├─ kinds.ts
72
+ │ │ ├─ presence-table.ts
73
+ │ │ ├─ presence.ts
74
+ │ │ ├─ redis-client.ts
75
+ │ │ ├─ store-memory.ts
76
+ │ │ ├─ store-redis.ts
77
+ │ │ ├─ subjects.ts
78
+ │ │ ├─ sync.ts
79
+ │ │ ├─ transport-nats.ts
80
+ │ │ └─ transport-redis.ts
81
+ │ ├─ hub/
82
+ │ │ ├─ context-factory.ts
83
+ │ │ ├─ dispatch.ts
84
+ │ │ ├─ index.ts
85
+ │ │ ├─ internal.ts
86
+ │ │ ├─ metrics-snapshot.ts
87
+ │ │ └─ resolve-cluster.ts
88
+ │ ├─ types/
89
+ │ │ ├─ client.ts
90
+ │ │ ├─ cluster.ts
91
+ │ │ ├─ context.ts
92
+ │ │ ├─ emit-target.ts
93
+ │ │ ├─ groups.ts
94
+ │ │ ├─ hub.ts
95
+ │ │ ├─ index.ts
96
+ │ │ ├─ metrics.ts
97
+ │ │ └─ options.ts
98
+ │ ├─ clients.ts
99
+ │ ├─ cluster-rpc.ts
100
+ │ ├─ data.ts
101
+ │ ├─ delivery.ts
102
+ │ ├─ emit.ts
103
+ │ ├─ global.ts
104
+ │ ├─ groups.ts
105
+ │ ├─ index.ts
106
+ │ ├─ queue.ts
107
+ │ ├─ registry.ts
108
+ │ ├─ schedule.ts
109
+ │ └─ trace.ts
110
+ ├─ generated/
111
+ │ ├─ fbs/
112
+ │ │ └─ backend.fbs
113
+ │ ├─ rust/
114
+ │ │ └─ backend_generated.rs
115
+ │ ├─ ts/
116
+ │ │ ├─ backend.ts
117
+ │ │ ├─ big-val.ts
118
+ │ │ ├─ complex.ts
119
+ │ │ ├─ customer.ts
120
+ │ │ ├─ hello.ts
121
+ │ │ ├─ join-group.ts
122
+ │ │ ├─ leave-group.ts
123
+ │ │ ├─ order-billing.ts
124
+ │ │ ├─ order-line.ts
125
+ │ │ ├─ order.ts
126
+ │ │ ├─ ping.ts
127
+ │ │ ├─ pong.ts
128
+ │ │ ├─ portfolio-position.ts
129
+ │ │ ├─ portfolio-snapshot.ts
130
+ │ │ ├─ quote.ts
131
+ │ │ ├─ resume.ts
132
+ │ │ ├─ resumed.ts
133
+ │ │ ├─ rpc-call.ts
134
+ │ │ ├─ rpc-result.ts
135
+ │ │ ├─ side.ts
136
+ │ │ ├─ snapshot-request.ts
137
+ │ │ ├─ subscribe.ts
138
+ │ │ ├─ tags.ts
139
+ │ │ ├─ trade.ts
140
+ │ │ ├─ unsubscribe.ts
141
+ │ │ └─ welcome.ts
142
+ │ ├─ direct-ser.ts
143
+ │ ├─ registry.ts
144
+ │ ├─ ts-ser.ts
145
+ │ └─ wire-registry.json
146
+ ├─ native/
147
+ │ ├─ codec.ts
148
+ │ ├─ ffi.ts
149
+ │ └─ loader.ts
150
+ ├─ schema/
151
+ │ └─ index.ts
152
+ ├─ transport/
153
+ │ ├─ byte-buffer-pool.ts
154
+ │ ├─ scratch.ts
155
+ │ └─ transport.ts
156
+ └─ server.ts
157
+ ```
158
+
159
+ ## rust/
160
+
161
+ ```
162
+ rust/
163
+ ├─ examples/
164
+ │ └─ dump.rs
165
+ ├─ src/
166
+ │ ├─ generated/
167
+ │ │ ├─ backend.rs
168
+ │ │ └─ mod.rs
169
+ │ ├─ transcode/
170
+ │ │ ├─ generated.rs
171
+ │ │ └─ mod.rs
172
+ │ ├─ ffi.rs
173
+ │ └─ lib.rs
174
+ ├─ .npmignore
175
+ ├─ Cargo.lock
176
+ └─ Cargo.toml
177
+ ```
178
+
179
+ ## public/
180
+
181
+ ```
182
+ public/
183
+ ├─ bindings.ts
184
+ ├─ client.ts
185
+ ├─ events.ts
186
+ ├─ generate.ts
187
+ ├─ internal.ts
188
+ ├─ nats.ts
189
+ └─ server.ts
190
+ ```
191
+
192
+ ## client/
193
+
194
+ ```
195
+ client/
196
+ ├─ index.html
197
+ └─ main.ts
198
+ ```
199
+
200
+ ## test/
201
+
202
+ ```
203
+ test/
204
+ ├─ api.test.ts
205
+ ├─ auth.test.ts
206
+ ├─ backpressure.test.ts
207
+ ├─ bidirectional.test.ts
208
+ ├─ bindings-gen.test.ts
209
+ ├─ byte-buffer-pool.test.ts
210
+ ├─ cluster-v2.test.ts
211
+ ├─ data-integrity-edge.test.ts
212
+ ├─ direct.test.ts
213
+ ├─ e2e.test.ts
214
+ ├─ efficiency.test.ts
215
+ ├─ events-cluster.test.ts
216
+ ├─ events.test.ts
217
+ ├─ ffi.test.ts
218
+ ├─ groups.test.ts
219
+ ├─ helpers.ts
220
+ ├─ int64.test.ts
221
+ ├─ integrity.test.ts
222
+ ├─ loader.test.ts
223
+ ├─ metrics.test.ts
224
+ ├─ nats-bridge.test.ts
225
+ ├─ nats-integration.test.ts
226
+ ├─ performance.test.ts
227
+ ├─ reconnect.test.ts
228
+ ├─ resume.test.ts
229
+ ├─ ring.test.ts
230
+ ├─ rooms.test.ts
231
+ ├─ roundtrip.test.ts
232
+ ├─ rpc.test.ts
233
+ ├─ security-hardening.test.ts
234
+ ├─ security-live-fuzz.test.ts
235
+ ├─ security.test.ts
236
+ ├─ stability-resilience.test.ts
237
+ ├─ targeting.test.ts
238
+ ├─ trace.test.ts
239
+ └─ wire.test.ts
240
+ ```
241
+
242
+ ## bench/
243
+
244
+ ```
245
+ bench/
246
+ ├─ BASELINE.md
247
+ ├─ dispatch.ts
248
+ ├─ ffi-margin.ts
249
+ ├─ measure.ts
250
+ ├─ serialize.ts
251
+ └─ throughput.ts
252
+ ```
253
+
254
+ ## scripts/
255
+
256
+ ```
257
+ scripts/
258
+ ├─ build-prebuild.ts
259
+ ├─ check-pack.ts
260
+ ├─ gen-ai-map.ts
261
+ ├─ generate.ts
262
+ └─ release.ts
263
+ ```
264
+
265
+ ## examples/
266
+
267
+ ```
268
+ examples/
269
+ ├─ consumer.ts
270
+ ├─ events.ts
271
+ ├─ generic-schema.ts
272
+ └─ nats-consumer.ts
273
+ ```
274
+
275
+ ## docs/
276
+
277
+ ```
278
+ docs/
279
+ ├─ ai/
280
+ │ ├─ LOCAL_DEV.md
281
+ │ └─ TREE.md
282
+ ├─ architecture.md
283
+ ├─ events.md
284
+ ├─ generic-bindings.md
285
+ ├─ publishing.md
286
+ └─ wire-format.md
287
+ ```
288
+
289
+ ## Notes
290
+
291
+ - `SKIP` set in the script excludes build output (target/, dist/, client-dist/, prebuilds/, node_modules/).
292
+ - `src/generated/`, `rust/src/generated/`, `rust/src/transcode/generated.rs` are regenerated by `bun run generate` — never hand-edit.