@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
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `assembleBindings` — build a runtime `Bindings` from generated parts.
3
+ *
4
+ * The generated artifacts (`registry.ts`, `ts-ser.ts`, `direct-ser.ts`) are
5
+ * schema-specific SOURCE produced by `generateBindings` (public/generate.ts).
6
+ * This function wires them into the single `Bindings` object the server /
7
+ * client / NATS bridge accept. It is also what the built-in registry uses
8
+ * (`src/bindings/default.ts`).
9
+ */
10
+ import type { TSchema } from "@sinclair/typebox";
11
+ import type { Bindings, DirectTables } from "./types";
12
+
13
+ /** Everything `assembleBindings` needs from the generated artifacts. */
14
+ export interface BindingsParts {
15
+ readonly wireVersion: number;
16
+ readonly wireHeaderLen: number;
17
+ readonly schemaFingerprint: number;
18
+ readonly eventNameToId: Readonly<Record<string, number>>;
19
+ readonly idToEventName: Readonly<Record<number, string>>;
20
+ readonly anyEventNameToId: Readonly<Record<string, number>>;
21
+ readonly idToAnyEventName: Readonly<Record<number, string>>;
22
+ readonly controlEventNameToId: Readonly<Record<string, number>>;
23
+ readFrameHeader(bytes: Uint8Array): { name: string; id: number } | null;
24
+ isControlId(id: number): boolean;
25
+ decodePayload(id: number, bytes: Uint8Array): unknown;
26
+ decodeFrame(bytes: Uint8Array): { name: string; id: number; payload: unknown } | null;
27
+ encodeFrame(name: string, payload: unknown): Uint8Array;
28
+ readonly direct?: DirectTables;
29
+ }
30
+
31
+ export interface AssembleOptions {
32
+ /** "required" | "optional" — see `Bindings.ffiMode`. */
33
+ ffiMode?: "required" | "optional";
34
+ /** NATS subject prefix for bridges built from these bindings. */
35
+ subjectPrefix?: string;
36
+ }
37
+
38
+ /**
39
+ * Wire generated parts + the schema registry into a `Bindings` object.
40
+ * `schema.events` / `schema.controlEvents` are the user's TypeBox schemas —
41
+ * they power the `EventsOf<B>` type derivation on the public API.
42
+ */
43
+ export function assembleBindings<
44
+ E extends Record<string, TSchema>,
45
+ C extends Record<string, TSchema>,
46
+ >(
47
+ parts: BindingsParts,
48
+ schema: { events: E; controlEvents?: C },
49
+ opts: AssembleOptions = {},
50
+ ): Omit<Bindings, "events" | "controlEvents"> & { events: E; controlEvents: C } {
51
+ const controlEvents = (schema.controlEvents ?? {}) as C;
52
+ const controlIds = new Set<number>(Object.values(parts.controlEventNameToId));
53
+ return {
54
+ wireVersion: parts.wireVersion,
55
+ wireHeaderLen: parts.wireHeaderLen,
56
+ schemaFingerprint: parts.schemaFingerprint,
57
+ ...(opts.subjectPrefix !== undefined ? { subjectPrefix: opts.subjectPrefix } : {}),
58
+ ffiMode: opts.ffiMode ?? "optional",
59
+ events: schema.events,
60
+ controlEvents,
61
+ eventNameToId: parts.eventNameToId,
62
+ idToEventName: parts.idToEventName,
63
+ anyEventNameToId: parts.anyEventNameToId,
64
+ idToAnyEventName: parts.idToAnyEventName,
65
+ controlIds,
66
+ readFrameHeader: parts.readFrameHeader,
67
+ isControlId: parts.isControlId,
68
+ decodePayload: parts.decodePayload,
69
+ decodeFrame: parts.decodeFrame,
70
+ encodeFrame: parts.encodeFrame,
71
+ ...(parts.direct ? { direct: parts.direct } : {}),
72
+ };
73
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The built-in `Bindings` — assembled from the repo's generated artifacts
3
+ * (`src/generated/*` + `src/schema`). This is the default for every entrypoint
4
+ * (`createServer` / `createClient` / `createNatsBridge`), so all existing code
5
+ * keeps working without passing `bindings`.
6
+ *
7
+ * For your own schema, see `generateBindings` (public/generate.ts) —
8
+ * `defaultBindings` is just the first, built-in instance of the same contract.
9
+ */
10
+
11
+ import {
12
+ directEncoders,
13
+ directSelfTest,
14
+ directSymbolNames,
15
+ directSymbols,
16
+ hasNulEncoders,
17
+ } from "../generated/direct-ser";
18
+ import {
19
+ anyEventNameToId,
20
+ controlEventNameToId,
21
+ decodeFrame,
22
+ decodePayload,
23
+ eventNameToId,
24
+ idToAnyEventName,
25
+ idToEventName,
26
+ isControlId,
27
+ readFrameHeader,
28
+ SCHEMA_FINGERPRINT,
29
+ WIRE_HEADER_LEN,
30
+ WIRE_VERSION,
31
+ } from "../generated/registry";
32
+ import { encodeEventFrame } from "../generated/ts-ser";
33
+ import { controlEvents, events } from "../schema";
34
+ import { assembleBindings } from "./assemble";
35
+
36
+ // NOTE: no explicit `: Bindings` annotation on purpose — the concrete
37
+ // `events` / `controlEvents` schema types must survive inference so
38
+ // `DefaultBindings` (and therefore `EventNameOf` / `EventsOf` on the default
39
+ // API) resolves to the built-in `Events` map.
40
+ export const defaultBindings = assembleBindings(
41
+ {
42
+ wireVersion: WIRE_VERSION,
43
+ wireHeaderLen: WIRE_HEADER_LEN,
44
+ schemaFingerprint: SCHEMA_FINGERPRINT,
45
+ eventNameToId,
46
+ idToEventName,
47
+ anyEventNameToId,
48
+ idToAnyEventName,
49
+ controlEventNameToId,
50
+ readFrameHeader,
51
+ isControlId,
52
+ decodePayload,
53
+ decodeFrame,
54
+ encodeFrame: encodeEventFrame as (name: string, payload: unknown) => Uint8Array,
55
+ direct: {
56
+ symbols: directSymbols,
57
+ symbolNames: directSymbolNames,
58
+ encoders: directEncoders,
59
+ hasNul: hasNulEncoders,
60
+ selfTest: directSelfTest,
61
+ },
62
+ },
63
+ { events, controlEvents },
64
+ { ffiMode: "required", subjectPrefix: "ignex" },
65
+ );
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Runtime `Bindings` — the per-schema bundle that decouples the transport from
3
+ * ANY particular event registry.
4
+ *
5
+ * Today the transport modules import the repo's generated artifacts directly
6
+ * (`src/generated/registry`, `src/generated/ts-ser`, `src/generated/direct-ser`
7
+ * and `src/schema`). `Bindings` is the generic contract: every schema-specific
8
+ * piece of the wire stack (event ids, decoders, encoders, direct fast-path
9
+ * tables, schema metadata) is grouped into one object, and the server / client /
10
+ * NATS bridge accept it via `options.bindings` (defaulting to the built-in
11
+ * registry, so existing code keeps working unchanged).
12
+ *
13
+ * Two ways to obtain a `Bindings`:
14
+ * - the built-in one: `defaultBindings` (see `src/bindings/default.ts`)
15
+ * - your own schema: run `generateBindings(schema)` (public/generate.ts),
16
+ * then assemble the emitted parts with `assembleBindings`
17
+ * (src/bindings/assemble.ts). Everything else — encode, decode, NATS
18
+ * subject naming, server/client APIs — is then typed against YOUR events.
19
+ */
20
+ import type { Static, TSchema } from "@sinclair/typebox";
21
+
22
+ /** Direct fast-path call signature (generated `direct-ser.ts`). */
23
+ export type DirectCall = (...args: unknown[]) => number;
24
+
25
+ /** Generated zero-alloc encoder: fields → FFI args → out buffer. */
26
+ export type DirectEncoder = (call: DirectCall, o: unknown, out: Uint8Array) => number;
27
+
28
+ /**
29
+ * The generated direct fast-path tables (Bun server only). Absent when the
30
+ * schema has no directable events or the codegen was run with `rust: false`.
31
+ */
32
+ export interface DirectTables {
33
+ /** dlopen specs in canonical form (see `src/native/ffi.ts` `abi()`). */
34
+ readonly symbols: Readonly<Record<string, { args: readonly string[]; returns: string }>>;
35
+ /** event → FFI symbol name. */
36
+ readonly symbolNames: Readonly<Record<string, string>>;
37
+ /** event → zero-alloc encoder. */
38
+ readonly encoders: Readonly<Record<string, DirectEncoder>>;
39
+ /** event → NUL pre-scan (true routes the payload to the JSON path). */
40
+ readonly hasNul: Readonly<Record<string, (o: unknown) => boolean>>;
41
+ /** bind-time per-symbol self-test; returns the symbol names to DISABLE. */
42
+ readonly selfTest: (
43
+ raw: Record<string, (...args: unknown[]) => number>,
44
+ scratch: Uint8Array,
45
+ ) => string[];
46
+ }
47
+
48
+ /**
49
+ * The complete per-schema wire stack. `events` / `controlEvents` are the
50
+ * TypeBox schemas — from them consumers derive `EventNameOf` / `EventsOf`.
51
+ */
52
+ export interface Bindings {
53
+ /** wire envelope version (see `scripts/constants.ts`). */
54
+ readonly wireVersion: number;
55
+ /** envelope header bytes: `[version:1][event_id:u32 LE]`. */
56
+ readonly wireHeaderLen: number;
57
+ /**
58
+ * Stable schema fingerprint (FNV-1a 32 over the canonical model). The Rust
59
+ * cdylib exports the same value (`fb_schema_fingerprint`), so a schema-
60
+ * mismatched addon fails the bind-time self-test instead of producing
61
+ * undecodable frames.
62
+ */
63
+ readonly schemaFingerprint: number;
64
+ /** NATS subject prefix used by bridges built from these bindings. */
65
+ readonly subjectPrefix?: string;
66
+ /**
67
+ * "required" — the Rust addon must exist and pass self-tests (the built-in
68
+ * registry: a missing addon throws). "optional" — generated for user
69
+ * schemas: the addon is used when `IGNEX_FFI_PATH` is set and passes
70
+ * self-tests; otherwise the pure-JS encoder is used (works without Rust).
71
+ */
72
+ readonly ffiMode: "required" | "optional";
73
+ /** app event schemas (name → TypeBox). */
74
+ readonly events: Readonly<Record<string, TSchema>>;
75
+ /** control event schemas (name → TypeBox). */
76
+ readonly controlEvents: Readonly<Record<string, TSchema>>;
77
+
78
+ // ── event id maps (stable FNV-1a 32 over the name) ────────────────────
79
+ readonly eventNameToId: Readonly<Record<string, number>>;
80
+ readonly idToEventName: Readonly<Record<number, string>>;
81
+ /** merged app + control maps (encode dispatch). */
82
+ readonly anyEventNameToId: Readonly<Record<string, number>>;
83
+ readonly idToAnyEventName: Readonly<Record<number, string>>;
84
+ readonly controlIds: ReadonlySet<number>;
85
+
86
+ // ── wire helpers (pure — run in browser + Bun) ────────────────────────
87
+ readFrameHeader(bytes: Uint8Array): { name: string; id: number } | null;
88
+ isControlId(id: number): boolean;
89
+ decodePayload(id: number, bytes: Uint8Array): unknown;
90
+ decodeFrame(bytes: Uint8Array): { name: string; id: number; payload: unknown } | null;
91
+
92
+ /** Pure-JS encoder: plain object → full wire frame. Works everywhere. */
93
+ encodeFrame(name: string, payload: unknown): Uint8Array;
94
+
95
+ /** Generated direct fast-path tables (optional — server only). */
96
+ readonly direct?: DirectTables;
97
+ }
98
+
99
+ // ── type-level derivation from a concrete Bindings ────────────────────────
100
+
101
+ export type EventNameOf<B extends Bindings> = Extract<keyof B["events"], string>;
102
+ export type ControlEventNameOf<B extends Bindings> = Extract<keyof B["controlEvents"], string>;
103
+
104
+ /** Plain-object payload map derived from a bindings' TypeBox schemas. */
105
+ export type EventsOf<B extends Bindings> = {
106
+ [K in EventNameOf<B>]: Static<B["events"][K]>;
107
+ };
108
+ export type ControlEventsOf<B extends Bindings> = {
109
+ [K in ControlEventNameOf<B>]: Static<B["controlEvents"][K]>;
110
+ };
111
+
112
+ /** The built-in registry's bindings type (see `src/bindings/default.ts`). */
113
+ export type DefaultBindings = typeof import("./default").defaultBindings;
@@ -11,14 +11,25 @@
11
11
  * inbound.>` and forwards decodable app events to `onInbound` (wired by the
12
12
  * server to fan out to clients). Control frames and unknown ids are dropped.
13
13
  *
14
+ * HORIZONTAL SCALING: when `bridgeClientEvents` is set, the server re-publishes
15
+ * every accepted client-sent event to `{prefix}.inbound.<event>` so OTHER
16
+ * server instances (and BE consumers) receive it — a cluster of servers sharing
17
+ * a prefix behaves as one hub (see docs/generic-bindings.md). Loop prevention:
18
+ * frames that arrive via NATS are forwarded to clients through `onInbound` and
19
+ * never re-bridged.
20
+ *
21
+ * GENERIC: `createNatsBridge(opts, transport?, bindings?)` decodes inbound
22
+ * frames with the given `Bindings` (default: the built-in registry), so the
23
+ * bridge works for ANY schema — the same wire bytes the server speaks.
24
+ *
14
25
  * The connection is created eagerly but non-blocking: `connect()` runs in the
15
26
  * background with a retry loop, so a server can start while NATS is down.
16
27
  * `createNatsBridge(opts, transport?)` accepts an injectable `NatsTransport`
17
28
  * so tests can fake NATS entirely (no server needed in CI).
18
29
  */
19
30
  import { connect, type NatsConnection } from "nats";
20
- import { decodePayload, isControlId, readFrameHeader } from "../generated/registry";
21
- import type { EventName } from "../schema";
31
+ import { defaultBindings } from "../bindings/default";
32
+ import type { Bindings } from "../bindings/types";
22
33
  import { createSubjectBuilder, type SubjectBuilder } from "./subjects";
23
34
 
24
35
  export type NatsBridgeStatus = "connected" | "connecting" | "closed";
@@ -26,8 +37,13 @@ export type NatsBridgeStatus = "connected" | "connecting" | "closed";
26
37
  export interface NatsBridgeOptions {
27
38
  /** NATS servers, default ["nats://localhost:4222"] */
28
39
  servers?: string[];
29
- /** subject prefix, default "ignex" */
40
+ /** subject prefix, default "ignex" (or the bindings' subjectPrefix) */
30
41
  subjectPrefix?: string;
42
+ /**
43
+ * The wire stack used to decode inbound frames (default: built-in registry).
44
+ * Pass your own generated bindings so the bridge decodes YOUR events.
45
+ */
46
+ bindings?: Bindings;
31
47
  /** connect timeout (ms), default 5000 */
32
48
  connectTimeout?: number;
33
49
  /** how long to wait before retrying a failed initial connect (ms), default 2000 */
@@ -41,7 +57,13 @@ export interface NatsBridgeOptions {
41
57
  /** inbound subjects (default `{prefix}.inbound.>`), requires `inbound` */
42
58
  inboundSubjects?: string[];
43
59
  /** only forward these inbound events (default: every app event) */
44
- inboundEvents?: EventName[];
60
+ inboundEvents?: string[];
61
+ /**
62
+ * Re-publish every accepted client-sent event to `{prefix}.inbound.<event>`
63
+ * so other servers in the cluster (and BE consumers) receive it, default
64
+ * false. See the horizontal-scaling docs.
65
+ */
66
+ bridgeClientEvents?: boolean;
45
67
  }
46
68
 
47
69
  /** Counters folded into `server.getMetrics()`. */
@@ -67,10 +89,18 @@ export interface NatsBridge {
67
89
  readonly status: NatsBridgeStatus;
68
90
  readonly subjects: SubjectBuilder;
69
91
  readonly stats: NatsBridgeStats;
92
+ /** whether client-sent events are re-published to `{prefix}.inbound.<event>` */
93
+ readonly clientEvents: boolean;
70
94
  /** publish a frame to `subject` (copies the bytes — safe after scratch reuse) */
71
95
  publish(subject: string, frame: Uint8Array): void;
96
+ /**
97
+ * Raw byte subscription (used by the events cluster layer). Unlike the
98
+ * inbound path this does NOT decode or forward — bytes are handed to `cb`
99
+ * verbatim, re-subscribed automatically after a NATS reconnect.
100
+ */
101
+ subscribeRaw(subject: string, cb: (data: Uint8Array) => void): () => void;
72
102
  /** wire the inbound → clients forward (set once by the server) */
73
- setOnInbound(cb: (name: EventName, payload: unknown) => void): void;
103
+ setOnInbound(cb: (name: string, payload: unknown) => void): void;
74
104
  close(): Promise<void>;
75
105
  }
76
106
 
@@ -129,7 +159,7 @@ function createRealTransport(opts: NatsBridgeOptions): NatsTransport {
129
159
  try {
130
160
  const conn = await connect({
131
161
  servers: opts.servers ?? ["nats://localhost:4222"],
132
- token: opts.token,
162
+ ...(opts.token !== undefined ? { token: opts.token } : {}),
133
163
  timeout: opts.connectTimeout ?? 5000,
134
164
  reconnect: opts.reconnect ?? true,
135
165
  maxReconnectAttempts: -1,
@@ -185,9 +215,11 @@ function createRealTransport(opts: NatsBridgeOptions): NatsTransport {
185
215
  export function createNatsBridge(
186
216
  opts: NatsBridgeOptions = {},
187
217
  transport?: NatsTransport,
218
+ bindings?: Bindings,
188
219
  ): NatsBridge {
220
+ const b = bindings ?? opts.bindings ?? defaultBindings;
189
221
  const t = transport ?? createRealTransport(opts);
190
- const subjects = createSubjectBuilder(opts.subjectPrefix);
222
+ const subjects = createSubjectBuilder(opts.subjectPrefix ?? b.subjectPrefix ?? "ignex");
191
223
  const stats: NatsBridgeStats = {
192
224
  bridged: 0,
193
225
  bridgedBytes: 0,
@@ -196,26 +228,26 @@ export function createNatsBridge(
196
228
  bridgeInboundErrors: 0,
197
229
  };
198
230
  let closed = false;
199
- let onInbound: ((name: EventName, payload: unknown) => void) | null = null;
231
+ let onInbound: ((name: string, payload: unknown) => void) | null = null;
200
232
  const allowlist = opts.inboundEvents ? new Set(opts.inboundEvents) : null;
201
233
 
202
234
  // inbound subscriptions (lazy — the transport queues them until connected)
203
235
  const subscribeInbound = (subject: string): (() => void) => {
204
236
  return t.subscribe(subject, (data) => {
205
- const header = readFrameHeader(data);
237
+ const header = b.readFrameHeader(data);
206
238
  if (!header) {
207
239
  stats.bridgeInboundErrors++;
208
240
  return;
209
241
  }
210
- if (isControlId(header.id)) {
242
+ if (b.isControlId(header.id)) {
211
243
  stats.bridgeInboundErrors++; // never forward transport-internal frames
212
244
  return;
213
245
  }
214
- const name = header.name as EventName;
246
+ const name = header.name;
215
247
  if (allowlist && !allowlist.has(name)) return;
216
248
  let payload: unknown;
217
249
  try {
218
- payload = decodePayload(header.id, data);
250
+ payload = b.decodePayload(header.id, data);
219
251
  } catch {
220
252
  stats.bridgeInboundErrors++;
221
253
  return;
@@ -227,7 +259,9 @@ export function createNatsBridge(
227
259
 
228
260
  const unsubs: Array<() => void> = [];
229
261
  if (opts.inbound) {
230
- const subjectsList = opts.inboundSubjects?.length ? opts.inboundSubjects : [subjects.inboundPrefix()];
262
+ const subjectsList = opts.inboundSubjects?.length
263
+ ? opts.inboundSubjects
264
+ : [subjects.inboundPrefix()];
231
265
  for (const subject of subjectsList) unsubs.push(subscribeInbound(subject));
232
266
  }
233
267
 
@@ -242,6 +276,9 @@ export function createNatsBridge(
242
276
  get stats() {
243
277
  return stats;
244
278
  },
279
+ get clientEvents(): boolean {
280
+ return opts.bridgeClientEvents ?? false;
281
+ },
245
282
  publish(subject, frame) {
246
283
  if (!t.connected) {
247
284
  stats.bridgeErrors++;
@@ -260,6 +297,9 @@ export function createNatsBridge(
260
297
  setOnInbound(cb) {
261
298
  onInbound = cb;
262
299
  },
300
+ subscribeRaw(subject, cb) {
301
+ return t.subscribe(subject, (data) => cb(data));
302
+ },
263
303
  async close() {
264
304
  closed = true;
265
305
  for (const u of unsubs) u();
@@ -18,6 +18,8 @@ export interface SubjectBuilder {
18
18
  group(group: string, name: string): string;
19
19
  /** wildcard subject the server subscribes to for inbound events */
20
20
  inboundPrefix(): string;
21
+ /** concrete inbound subject for one event — used to re-publish client events into the cluster */
22
+ inboundEvent(name: string): string;
21
23
  }
22
24
 
23
25
  export function createSubjectBuilder(prefix = "ignex"): SubjectBuilder {
@@ -26,5 +28,6 @@ export function createSubjectBuilder(prefix = "ignex"): SubjectBuilder {
26
28
  topic: (topic, name) => `${prefix}.topic.${topic}.${name}`,
27
29
  group: (group, name) => `${prefix}.group.${group}.${name}`,
28
30
  inboundPrefix: () => `${prefix}.inbound.>`,
31
+ inboundEvent: (name) => `${prefix}.inbound.${name}`,
29
32
  };
30
33
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared wire-format constants. These are the SINGLE source of truth for the
3
+ * transport envelope — every emitter (TS registry, Rust glue, direct serde)
4
+ * imports them so the TS and Rust sides stay in sync by construction.
5
+ *
6
+ * The Rust `fb_wire_version()` export is checked against `WIRE_VERSION` at
7
+ * bind time (`src/native/ffi.ts`) to catch any drift between a stale cdylib
8
+ * and the generated artifacts.
9
+ */
10
+
11
+ /** Wire format version. Bump on any BREAKING envelope change. */
12
+ export const WIRE_VERSION = 1;
13
+
14
+ /**
15
+ * Envelope header length in bytes: `[version:1][event_id:u32 LE]`. The
16
+ * size-prefixed FlatBuffer payload follows immediately after.
17
+ */
18
+ export const WIRE_HEADER_LEN = 5;