@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.
- package/README.md +132 -32
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +232 -0
- package/docs/architecture.md +35 -10
- package/docs/events.md +170 -0
- package/docs/generic-bindings.md +197 -0
- package/docs/publishing.md +2 -2
- package/docs/wire-format.md +9 -2
- package/index.ts +75 -27
- package/package.json +12 -2
- package/prebuilds/linux-x64/libignex_ffi.so +0 -0
- package/public/bindings.ts +24 -0
- package/public/client.ts +5 -1
- package/public/events.ts +71 -0
- package/public/generate.ts +416 -0
- package/public/internal.ts +16 -0
- package/public/nats.ts +9 -5
- package/public/server.ts +42 -16
- package/rust/src/ffi.rs +10 -0
- package/rust/src/transcode/generated.rs +2 -1
- package/src/bindings/assemble.ts +73 -0
- package/src/bindings/default.ts +65 -0
- package/src/bindings/types.ts +113 -0
- package/src/bridge/nats.ts +53 -13
- package/src/bridge/subjects.ts +3 -0
- package/src/codegen/constants.ts +18 -0
- package/src/codegen/direct-gen.ts +550 -0
- package/src/codegen/fingerprint.ts +44 -0
- package/src/codegen/hash.ts +25 -0
- package/src/codegen/registry-gen.ts +242 -0
- package/src/codegen/rust-glue-gen.ts +545 -0
- package/src/codegen/schema-model.ts +338 -0
- package/src/codegen/ts-ser-gen.ts +221 -0
- package/src/codegen/typebox-to-fbs.ts +60 -0
- package/src/core/auth.ts +2 -1
- package/src/core/client-heartbeat.ts +2 -1
- package/src/core/client-reconnect.ts +9 -2
- package/src/core/client-state.ts +21 -8
- package/src/core/client-wire.ts +10 -11
- package/src/core/client.ts +34 -29
- package/src/core/groups.ts +3 -0
- package/src/core/metrics.ts +7 -3
- package/src/core/outbound.ts +12 -5
- package/src/core/routing.ts +17 -8
- package/src/core/server.ts +108 -34
- package/src/core/state.ts +51 -13
- package/src/events/clients.ts +156 -0
- package/src/events/cluster.ts +732 -0
- package/src/events/data.ts +38 -0
- package/src/events/emit.ts +127 -0
- package/src/events/global.ts +117 -0
- package/src/events/groups.ts +118 -0
- package/src/events/hub.ts +481 -0
- package/src/events/index.ts +61 -0
- package/src/events/queue.ts +96 -0
- package/src/events/registry.ts +178 -0
- package/src/events/types.ts +378 -0
- package/src/generated/direct-ser.ts +2 -1
- package/src/generated/fbs/backend.fbs +1 -1
- package/src/generated/registry.ts +3 -1
- package/src/generated/ts-ser.ts +1 -1
- package/src/generated/wire-registry.json +1 -0
- package/src/native/ffi.ts +85 -28
- package/src/schema/index.ts +5 -2
- package/src/server.ts +7 -3
- package/src/transport/stats.ts +8 -4
- 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;
|
package/src/bridge/nats.ts
CHANGED
|
@@ -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 {
|
|
21
|
-
import type {
|
|
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?:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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();
|
package/src/bridge/subjects.ts
CHANGED
|
@@ -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;
|