@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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler registry — the "events file receives events" core. An ordered,
|
|
3
|
+
* multi-handler-per-event registry with per-handler error isolation:
|
|
4
|
+
* one throwing handler never prevents the others, and every failure is
|
|
5
|
+
* counted (surfaced via `hub.metrics().handlerErrors`).
|
|
6
|
+
*
|
|
7
|
+
* Supports `on` / `off` / `once` / `onAny` / `removeAllListeners` and a
|
|
8
|
+
* separate server-event registry (`onServerEvent`) for events that arrive
|
|
9
|
+
* from other instances or the NATS bridge.
|
|
10
|
+
*
|
|
11
|
+
* The registry is BINDINGS-AGNOSTIC: names and payloads are `unknown` here,
|
|
12
|
+
* and the `ctx` is passed through opaquely (the hub types it as
|
|
13
|
+
* `EventContext<B>` at the boundary).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Opaque context — the hub hands a typed `EventContext<B>` through. */
|
|
17
|
+
export type DispatchContext = unknown;
|
|
18
|
+
|
|
19
|
+
type AnyHandler = (payload: unknown, ctx: DispatchContext) => void | Promise<void>;
|
|
20
|
+
|
|
21
|
+
export interface HandlerRegistry {
|
|
22
|
+
on(name: string, handler: AnyHandler): void;
|
|
23
|
+
off(name: string, handler?: AnyHandler): void;
|
|
24
|
+
once(name: string, handler: AnyHandler): void;
|
|
25
|
+
onAny(cb: AnyHandler): void;
|
|
26
|
+
offAny(cb: AnyHandler): void;
|
|
27
|
+
has(name: string): boolean;
|
|
28
|
+
names(): string[];
|
|
29
|
+
count(name: string): number;
|
|
30
|
+
removeAll(name?: string): void;
|
|
31
|
+
/** dispatch to every handler for `name` + every onAny handler. Isolated. */
|
|
32
|
+
dispatch(name: string, payload: unknown, ctx: DispatchContext): void;
|
|
33
|
+
/** register a server-side handler (remote / bridge events only) */
|
|
34
|
+
onServerEvent(name: string, handler: AnyHandler): void;
|
|
35
|
+
offServerEvent(name: string, handler?: AnyHandler): void;
|
|
36
|
+
/** true when a server-event handler exists for `name` (avoids payload decode). */
|
|
37
|
+
wantsServerEvent(name: string): boolean;
|
|
38
|
+
dispatchServerEvent(name: string, payload: unknown, ctx: DispatchContext): void;
|
|
39
|
+
onError(cb: (err: Error, name: string) => void): void;
|
|
40
|
+
/** total handler exceptions since creation */
|
|
41
|
+
readonly errorCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function invoke(handler: AnyHandler, payload: unknown, ctx: DispatchContext, onError: (err: Error) => void): void {
|
|
45
|
+
try {
|
|
46
|
+
const r = handler(payload, ctx);
|
|
47
|
+
if (r && typeof (r as Promise<void>).then === "function") {
|
|
48
|
+
void (r as Promise<void>).catch((err: unknown) => onError(err instanceof Error ? err : new Error(String(err))));
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createHandlerRegistry(): HandlerRegistry {
|
|
56
|
+
const handlers = new Map<string, Set<AnyHandler>>();
|
|
57
|
+
const anyHandlers = new Set<AnyHandler>();
|
|
58
|
+
const serverHandlers = new Map<string, Set<AnyHandler>>();
|
|
59
|
+
const errorCbs: Array<(err: Error, name: string) => void> = [];
|
|
60
|
+
let errorCount = 0;
|
|
61
|
+
|
|
62
|
+
const report = (err: Error, name: string): void => {
|
|
63
|
+
errorCount++;
|
|
64
|
+
for (const cb of errorCbs) {
|
|
65
|
+
try {
|
|
66
|
+
cb(err, name);
|
|
67
|
+
} catch {
|
|
68
|
+
// error reporters must never break dispatch
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const add = (name: string, handler: AnyHandler): void => {
|
|
74
|
+
let set = handlers.get(name);
|
|
75
|
+
if (!set) {
|
|
76
|
+
set = new Set();
|
|
77
|
+
handlers.set(name, set);
|
|
78
|
+
}
|
|
79
|
+
set.add(handler);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const addServer = (name: string, handler: AnyHandler): void => {
|
|
83
|
+
let set = serverHandlers.get(name);
|
|
84
|
+
if (!set) {
|
|
85
|
+
set = new Set();
|
|
86
|
+
serverHandlers.set(name, set);
|
|
87
|
+
}
|
|
88
|
+
set.add(handler);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
on(name, handler) {
|
|
93
|
+
add(name, handler);
|
|
94
|
+
},
|
|
95
|
+
off(name, handler) {
|
|
96
|
+
if (!handler) {
|
|
97
|
+
handlers.delete(name);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
handlers.get(name)?.delete(handler);
|
|
101
|
+
},
|
|
102
|
+
once(name, handler) {
|
|
103
|
+
const wrap: AnyHandler = (payload, ctx) => {
|
|
104
|
+
handlers.get(name)?.delete(wrap);
|
|
105
|
+
return handler(payload, ctx);
|
|
106
|
+
};
|
|
107
|
+
add(name, wrap);
|
|
108
|
+
},
|
|
109
|
+
onAny(cb) {
|
|
110
|
+
anyHandlers.add(cb);
|
|
111
|
+
},
|
|
112
|
+
offAny(cb) {
|
|
113
|
+
anyHandlers.delete(cb);
|
|
114
|
+
},
|
|
115
|
+
has(name) {
|
|
116
|
+
return handlers.has(name) || anyHandlers.size > 0;
|
|
117
|
+
},
|
|
118
|
+
names() {
|
|
119
|
+
return [...handlers.keys()];
|
|
120
|
+
},
|
|
121
|
+
count(name) {
|
|
122
|
+
return handlers.get(name)?.size ?? 0;
|
|
123
|
+
},
|
|
124
|
+
removeAll(name) {
|
|
125
|
+
if (name) handlers.delete(name);
|
|
126
|
+
else handlers.clear();
|
|
127
|
+
},
|
|
128
|
+
dispatch(name, payload, ctx) {
|
|
129
|
+
const set = handlers.get(name);
|
|
130
|
+
if (set) {
|
|
131
|
+
const snapshot = [...set];
|
|
132
|
+
for (const h of snapshot) {
|
|
133
|
+
invoke(h, payload, ctx, (err) => report(err, name));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (anyHandlers.size > 0) {
|
|
137
|
+
const snapshot = [...anyHandlers];
|
|
138
|
+
for (const h of snapshot) {
|
|
139
|
+
invoke(
|
|
140
|
+
(p) => {
|
|
141
|
+
(h as (n: string, p: unknown, c: DispatchContext) => unknown)(name, p, ctx);
|
|
142
|
+
},
|
|
143
|
+
payload,
|
|
144
|
+
ctx,
|
|
145
|
+
(err) => report(err, name),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
wantsServerEvent(name) {
|
|
151
|
+
return (serverHandlers.get(name)?.size ?? 0) > 0;
|
|
152
|
+
},
|
|
153
|
+
onServerEvent(name, handler) {
|
|
154
|
+
addServer(name, handler);
|
|
155
|
+
},
|
|
156
|
+
offServerEvent(name, handler) {
|
|
157
|
+
if (!handler) {
|
|
158
|
+
serverHandlers.delete(name);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
serverHandlers.get(name)?.delete(handler);
|
|
162
|
+
},
|
|
163
|
+
dispatchServerEvent(name, payload, ctx) {
|
|
164
|
+
const set = serverHandlers.get(name);
|
|
165
|
+
if (!set) return;
|
|
166
|
+
const snapshot = [...set];
|
|
167
|
+
for (const h of snapshot) {
|
|
168
|
+
invoke(h, payload, ctx, (err) => report(err, name));
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
onError(cb) {
|
|
172
|
+
errorCbs.push(cb);
|
|
173
|
+
},
|
|
174
|
+
get errorCount(): number {
|
|
175
|
+
return errorCount;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the events layer — the typed, event-driven surface on top
|
|
3
|
+
* of the FlatBuffer transport core.
|
|
4
|
+
*
|
|
5
|
+
* The events layer is the application-facing counterpart to `src/core/routing`:
|
|
6
|
+
* the server transports frames; this layer receives them (via handlers, "like
|
|
7
|
+
* routes are made") and emits them to websocket clients (the global `emit`),
|
|
8
|
+
* with first-class client records ("who is connected, on whose behalf, and
|
|
9
|
+
* what state does the app keep per connection"), named groups, and optional
|
|
10
|
+
* cross-instance sync for horizontally scaled deployments.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is type-only or interface-shaped; the runtime lives in the
|
|
13
|
+
* sibling modules (`clients`, `registry`, `groups`, `cluster`, `emit`, `hub`).
|
|
14
|
+
*/
|
|
15
|
+
import type { ServerWebSocket } from "bun";
|
|
16
|
+
import type { Bindings, DefaultBindings, EventNameOf, EventsOf } from "../bindings/types";
|
|
17
|
+
import type { IgnServer } from "../core/server";
|
|
18
|
+
import type { WsData } from "../core/state";
|
|
19
|
+
|
|
20
|
+
// ── client records ──────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Per-connection state store attached to an active client record. */
|
|
23
|
+
export interface ClientData {
|
|
24
|
+
/** Read a value previously `set` on this connection. */
|
|
25
|
+
get(key: string): unknown;
|
|
26
|
+
/** Store a value on this connection (arbitrary app state, per socket). */
|
|
27
|
+
set(key: string, value: unknown): void;
|
|
28
|
+
has(key: string): boolean;
|
|
29
|
+
delete(key: string): boolean;
|
|
30
|
+
clear(): void;
|
|
31
|
+
keys(): string[];
|
|
32
|
+
entries(): Array<[string, unknown]>;
|
|
33
|
+
toJSON(): Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An active user connection — the server-side representation of "who is
|
|
38
|
+
* connected, on whose behalf, and what to remember about them".
|
|
39
|
+
*
|
|
40
|
+
* - `id` is the connection id (the socket identity, unique per connection).
|
|
41
|
+
* - `userId` is the identity this connection acts ON BEHALF OF (set via the
|
|
42
|
+
* `authenticate` hook, `hub.setUserId`, or later); several connections may
|
|
43
|
+
* share a `userId` (multi-tab / multi-device), and `hub.clientsByUser`
|
|
44
|
+
* groups them.
|
|
45
|
+
* - `data` is the per-connection app store, cleared automatically on close.
|
|
46
|
+
* - `groups` / `topics` are shared with the transport (`ws.data`), so control
|
|
47
|
+
* frames (`joinGroup` / `subscribe`) stay consistent with the events layer.
|
|
48
|
+
*/
|
|
49
|
+
export interface EventClient {
|
|
50
|
+
/** stable connection id (ws identity, unique per socket) */
|
|
51
|
+
readonly id: string;
|
|
52
|
+
/** identity this connection acts on behalf of (undefined = anonymous) */
|
|
53
|
+
readonly userId: string | undefined;
|
|
54
|
+
/** arbitrary app metadata from `authenticate` (undefined if none) */
|
|
55
|
+
readonly meta: Record<string, unknown> | undefined;
|
|
56
|
+
/** per-connection app state store (auto-cleared on disconnect) */
|
|
57
|
+
readonly data: ClientData;
|
|
58
|
+
/** server-side client groups this connection belongs to */
|
|
59
|
+
readonly groups: ReadonlySet<string>;
|
|
60
|
+
/** topics/rooms this connection has joined */
|
|
61
|
+
readonly topics: ReadonlySet<string>;
|
|
62
|
+
/** epoch ms the socket connected */
|
|
63
|
+
readonly connectedAt: number;
|
|
64
|
+
/** remote IP (from the socket) */
|
|
65
|
+
readonly ip: string;
|
|
66
|
+
/** true after the socket closed (record is then detached) */
|
|
67
|
+
readonly closed: boolean;
|
|
68
|
+
/** the underlying socket (advanced / low-level use) */
|
|
69
|
+
readonly ws: ServerWebSocket<WsData>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A connection known to exist on ANOTHER instance (via cluster presence). */
|
|
73
|
+
export interface RemoteClient {
|
|
74
|
+
clientId: string;
|
|
75
|
+
/** the instance that reported this connection */
|
|
76
|
+
instanceId: string;
|
|
77
|
+
userId?: string;
|
|
78
|
+
/** epoch ms the connection was (re)confirmed by its instance */
|
|
79
|
+
lastSeen: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── emit targets ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Where an emit goes. The discriminated union is how the API "easily
|
|
86
|
+
* differentiates" between the addressing modes:
|
|
87
|
+
*
|
|
88
|
+
* - `{ type: "broadcast" }` — every connected client, on every instance.
|
|
89
|
+
* - `{ type: "topic", topic }` — subscribers of a topic (rooms + replay).
|
|
90
|
+
* - `{ type: "group", group }` — members of a server-side group.
|
|
91
|
+
* - `{ type: "user", userId }` — every socket acting on behalf of `userId`.
|
|
92
|
+
* - `{ type: "client", clientId }` — one specific connection.
|
|
93
|
+
*
|
|
94
|
+
* Local delivery is synchronous and allocation-free (the transport scratch +
|
|
95
|
+
* `ws.send` copy); the cross-instance fan-out (when a cluster is configured)
|
|
96
|
+
* is deferred to the offload queue so the emit call never blocks.
|
|
97
|
+
*/
|
|
98
|
+
export type EmitTarget =
|
|
99
|
+
| { type: "broadcast" }
|
|
100
|
+
| { type: "topic"; topic: string }
|
|
101
|
+
| { type: "group"; group: string }
|
|
102
|
+
| { type: "user"; userId: string }
|
|
103
|
+
| { type: "client"; clientId: string };
|
|
104
|
+
|
|
105
|
+
export type EmitTargetKind = EmitTarget["type"];
|
|
106
|
+
|
|
107
|
+
// ── event contexts ──────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/** Where an event reached the hub from. */
|
|
110
|
+
export type EventSource = "client" | "remote" | "bridge";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The context every handler receives — the "who / where / how do I reply"
|
|
114
|
+
* bundle. For client-sent events `client` is the sender's record; for
|
|
115
|
+
* server-side events (`onServerEvent`) there is no sender client.
|
|
116
|
+
*/
|
|
117
|
+
export interface EventContext<B extends Bindings = DefaultBindings> {
|
|
118
|
+
/** where the event came from: a local client, another instance, or the bridge */
|
|
119
|
+
readonly source: EventSource;
|
|
120
|
+
/** the client that sent the event (undefined for remote/bridge events) */
|
|
121
|
+
readonly client?: EventClient;
|
|
122
|
+
/** the events hub (for `hub.emit`, groups, client data, …) */
|
|
123
|
+
readonly hub: EventsHub<B>;
|
|
124
|
+
/** the underlying server (raw `publish`/`publishToClient`/… escape hatch) */
|
|
125
|
+
readonly server: IgnServer<B>;
|
|
126
|
+
/** emit helpers bound to this hub (reply without importing the singleton) */
|
|
127
|
+
emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K], target?: EmitTarget): void;
|
|
128
|
+
emitToGroup<K extends EventNameOf<B>>(group: string, name: K, payload: EventsOf<B>[K]): void;
|
|
129
|
+
emitToUser<K extends EventNameOf<B>>(userId: string, name: K, payload: EventsOf<B>[K]): void;
|
|
130
|
+
emitToClient<K extends EventNameOf<B>>(clientId: string, name: K, payload: EventsOf<B>[K]): void;
|
|
131
|
+
emitToTopic<K extends EventNameOf<B>>(topic: string, name: K, payload: EventsOf<B>[K]): void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A handler registered on the hub (client-sent events). May be async. */
|
|
135
|
+
export type EventHandler<B extends Bindings, K extends EventNameOf<B>> = (
|
|
136
|
+
payload: EventsOf<B>[K],
|
|
137
|
+
ctx: EventContext<B>,
|
|
138
|
+
) => void | Promise<void>;
|
|
139
|
+
|
|
140
|
+
/** A handler for server-side events (remote instances / bridge inbound). */
|
|
141
|
+
export type ServerEventHandler<B extends Bindings, K extends EventNameOf<B>> = (
|
|
142
|
+
payload: EventsOf<B>[K],
|
|
143
|
+
ctx: EventContext<B>,
|
|
144
|
+
) => void | Promise<void>;
|
|
145
|
+
|
|
146
|
+
// ── groups ──────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/** A named client group: membership by connection id, fan-out via the hub. */
|
|
149
|
+
export interface ClientGroup<B extends Bindings = DefaultBindings> {
|
|
150
|
+
readonly name: string;
|
|
151
|
+
/** add a connection (by id) to the group (idempotent) */
|
|
152
|
+
add(clientId: string): void;
|
|
153
|
+
remove(clientId: string): void;
|
|
154
|
+
has(clientId: string): boolean;
|
|
155
|
+
/** member connection ids */
|
|
156
|
+
members(): string[];
|
|
157
|
+
readonly size: number;
|
|
158
|
+
/** emit an event to every member of this group (cluster-aware) */
|
|
159
|
+
emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K]): void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A named USER group: membership by `userId`, fan-out to every socket of each member user. */
|
|
163
|
+
export interface UserGroup<B extends Bindings = DefaultBindings> {
|
|
164
|
+
readonly name: string;
|
|
165
|
+
add(userId: string): void;
|
|
166
|
+
remove(userId: string): void;
|
|
167
|
+
has(userId: string): boolean;
|
|
168
|
+
/** member user ids */
|
|
169
|
+
members(): string[];
|
|
170
|
+
readonly size: number;
|
|
171
|
+
/** emit an event to every socket acting on behalf of each member user */
|
|
172
|
+
emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K]): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── cluster sync ────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Cross-instance messaging transport (server ⇄ server). NATS and Redis
|
|
179
|
+
* adapters are provided; any broker that supports named channels + byte
|
|
180
|
+
* payloads can be plugged in (tests use an in-memory bus). All calls are
|
|
181
|
+
* fire-and-forget and are invoked from the offload queue, never from the WS
|
|
182
|
+
* hot path.
|
|
183
|
+
*/
|
|
184
|
+
export interface ClusterTransport {
|
|
185
|
+
readonly connected: boolean;
|
|
186
|
+
/** synchronously hand bytes to the broker (throws → caller counts an error) */
|
|
187
|
+
publish(subject: string, data: Uint8Array): void;
|
|
188
|
+
/** subscribe; `cb` receives raw message bytes; returns an unsubscribe fn */
|
|
189
|
+
subscribe(subject: string, cb: (data: Uint8Array) => void): () => void;
|
|
190
|
+
close(): Promise<void>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Optional shared-state store (presence / cluster group membership / client
|
|
195
|
+
* data). A memory implementation is used by default (per-instance); Redis is
|
|
196
|
+
* the production choice for horizontally scaled deployments (`createRedisStateStore`).
|
|
197
|
+
*/
|
|
198
|
+
export interface ClusterStateStore {
|
|
199
|
+
get(key: string): Promise<string | null>;
|
|
200
|
+
set(key: string, value: string, ttlMs?: number): Promise<void>;
|
|
201
|
+
del(key: string): Promise<void>;
|
|
202
|
+
sadd(key: string, member: string): Promise<void>;
|
|
203
|
+
srem(key: string, member: string): Promise<void>;
|
|
204
|
+
smembers(key: string): Promise<string[]>;
|
|
205
|
+
expire(key: string, ttlMs: number): Promise<void>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Redis connection options — a URL string or an ioredis options object. */
|
|
209
|
+
export type RedisConnectionOptions = string | Record<string, unknown>;
|
|
210
|
+
|
|
211
|
+
// ── metrics ─────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/** Events-layer counters (folded into `server.getMetrics().events`). */
|
|
214
|
+
export interface EventsMetricsSnapshot {
|
|
215
|
+
/** emit calls (all targets) */
|
|
216
|
+
emitted: number;
|
|
217
|
+
emittedByTarget: Record<EmitTargetKind, number>;
|
|
218
|
+
/** frames written to local sockets by the events layer */
|
|
219
|
+
deliveredLocal: number;
|
|
220
|
+
/** frames handed to the cluster transport */
|
|
221
|
+
clusterPublished: number;
|
|
222
|
+
/** frames received from other instances */
|
|
223
|
+
clusterReceived: number;
|
|
224
|
+
/** frames dropped because they originated on this instance */
|
|
225
|
+
clusterDroppedSelf: number;
|
|
226
|
+
/** cluster transport / decode failures */
|
|
227
|
+
clusterErrors: number;
|
|
228
|
+
/** offload-queue: tasks accepted */
|
|
229
|
+
queueQueued: number;
|
|
230
|
+
/** offload-queue: tasks dropped (overflow) */
|
|
231
|
+
queueDropped: number;
|
|
232
|
+
/** offload-queue: task failures */
|
|
233
|
+
queueErrors: number;
|
|
234
|
+
/** handler exceptions (caught + isolated) */
|
|
235
|
+
handlerErrors: number;
|
|
236
|
+
/** local connected clients */
|
|
237
|
+
connectedClients: number;
|
|
238
|
+
/** remote clients known via cluster presence */
|
|
239
|
+
remoteClients: number;
|
|
240
|
+
/** active client groups / user groups */
|
|
241
|
+
clientGroups: number;
|
|
242
|
+
userGroups: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── hub ─────────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The events hub — the public API returned as `server.events` when
|
|
249
|
+
* `createServer({ events: {...} })` is used, and the backing store for the
|
|
250
|
+
* module-global `emit` / `on` singleton (`ignex-nova/events`).
|
|
251
|
+
*/
|
|
252
|
+
export interface EventsHub<B extends Bindings = DefaultBindings> {
|
|
253
|
+
readonly server: IgnServer<B>;
|
|
254
|
+
/** stable id of THIS instance (self-delivery dedupe in a cluster) */
|
|
255
|
+
readonly instanceId: string;
|
|
256
|
+
|
|
257
|
+
// ── receiving events (the "events file": where events come in) ────────
|
|
258
|
+
on<K extends EventNameOf<B>>(name: K, handler: EventHandler<B, K>): EventsHub<B>;
|
|
259
|
+
off<K extends EventNameOf<B>>(name: K, handler?: EventHandler<B, K>): EventsHub<B>;
|
|
260
|
+
once<K extends EventNameOf<B>>(name: K, handler: EventHandler<B, K>): EventsHub<B>;
|
|
261
|
+
/** every client-sent inbound event (name + payload + ctx) */
|
|
262
|
+
onAny(cb: (name: EventNameOf<B>, payload: unknown, ctx: EventContext<B>) => void): EventsHub<B>;
|
|
263
|
+
offAny(cb: (name: EventNameOf<B>, payload: unknown, ctx: EventContext<B>) => void): EventsHub<B>;
|
|
264
|
+
/** server-side handlers for events from OTHER instances / the bridge */
|
|
265
|
+
onServerEvent<K extends EventNameOf<B>>(name: K, handler: ServerEventHandler<B, K>): EventsHub<B>;
|
|
266
|
+
offServerEvent<K extends EventNameOf<B>>(
|
|
267
|
+
name: K,
|
|
268
|
+
handler?: ServerEventHandler<B, K>,
|
|
269
|
+
): EventsHub<B>;
|
|
270
|
+
/** event names with at least one handler */
|
|
271
|
+
events(): EventNameOf<B>[];
|
|
272
|
+
listenerCount(name: EventNameOf<B>): number;
|
|
273
|
+
removeAllListeners(name?: EventNameOf<B>): EventsHub<B>;
|
|
274
|
+
|
|
275
|
+
// ── emitting events (through websockets, cluster-aware) ───────────────
|
|
276
|
+
emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K], target?: EmitTarget): void;
|
|
277
|
+
emitToTopic<K extends EventNameOf<B>>(topic: string, name: K, payload: EventsOf<B>[K]): void;
|
|
278
|
+
emitToGroup<K extends EventNameOf<B>>(group: string, name: K, payload: EventsOf<B>[K]): void;
|
|
279
|
+
emitToUser<K extends EventNameOf<B>>(userId: string, name: K, payload: EventsOf<B>[K]): void;
|
|
280
|
+
emitToClient<K extends EventNameOf<B>>(clientId: string, name: K, payload: EventsOf<B>[K]): void;
|
|
281
|
+
|
|
282
|
+
// ── client records ("who is connected, on whose behalf") ──────────────
|
|
283
|
+
client(id: string): EventClient | undefined;
|
|
284
|
+
clients(): EventClient[];
|
|
285
|
+
/** every connection acting on behalf of `userId` */
|
|
286
|
+
clientsByUser(userId: string): EventClient[];
|
|
287
|
+
readonly clientCount: number;
|
|
288
|
+
/** bind a connection to an identity (on whose behalf it acts) */
|
|
289
|
+
setUserId(clientId: string, userId: string): void;
|
|
290
|
+
/** per-connection app state */
|
|
291
|
+
setClientData(clientId: string, key: string, value: unknown): void;
|
|
292
|
+
getClientData(clientId: string, key: string): unknown;
|
|
293
|
+
clearClientData(clientId: string): void;
|
|
294
|
+
|
|
295
|
+
// ── groups ────────────────────────────────────────────────────────────
|
|
296
|
+
/** client group handle (membership by connection id) */
|
|
297
|
+
group(name: string): ClientGroup<B>;
|
|
298
|
+
/** live client-group names */
|
|
299
|
+
groups(): string[];
|
|
300
|
+
/** user group handle (membership by userId, fan-out to every socket) */
|
|
301
|
+
userGroup(name: string): UserGroup<B>;
|
|
302
|
+
/** live user-group names */
|
|
303
|
+
userGroups(): string[];
|
|
304
|
+
|
|
305
|
+
// ── horizontal scaling ────────────────────────────────────────────────
|
|
306
|
+
/** connections known on other instances (presence; [] when unclustered) */
|
|
307
|
+
clusterClients(): RemoteClient[];
|
|
308
|
+
/** user→clients index from the shared state store, if configured */
|
|
309
|
+
clusterUserClients(userId: string): Promise<Array<{ instanceId: string; clientId: string }>>;
|
|
310
|
+
/** cluster-wide client-group members (shared state store), if configured */
|
|
311
|
+
clusterGroupMembers(group: string): Promise<string[]>;
|
|
312
|
+
/** cluster-wide user-group members (shared state store), if configured */
|
|
313
|
+
clusterUserGroupMembers(group: string): Promise<string[]>;
|
|
314
|
+
/** client data from the shared state store, if configured */
|
|
315
|
+
remoteClientData(clientId: string): Promise<Record<string, unknown> | undefined>;
|
|
316
|
+
|
|
317
|
+
// ── lifecycle / observability ─────────────────────────────────────────
|
|
318
|
+
metrics(): EventsMetricsSnapshot;
|
|
319
|
+
queueStats(): {
|
|
320
|
+
pending: number;
|
|
321
|
+
queued: number;
|
|
322
|
+
processed: number;
|
|
323
|
+
dropped: number;
|
|
324
|
+
errors: number;
|
|
325
|
+
};
|
|
326
|
+
close(): Promise<void>;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── options ─────────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* `createServer({ events: EventsOptions })` — enables the events layer and
|
|
333
|
+
* exposes it as `server.events`. The module-global `emit` / `on` singleton is
|
|
334
|
+
* bound by default (`global: false` to opt out).
|
|
335
|
+
*/
|
|
336
|
+
export interface EventsOptions<B extends Bindings = DefaultBindings> {
|
|
337
|
+
/**
|
|
338
|
+
* App events clients are allowed to send to the hub. Default: every event
|
|
339
|
+
* registered via `hub.on(...)` is auto-allowed on first use.
|
|
340
|
+
*/
|
|
341
|
+
inbound?: EventNameOf<B>[];
|
|
342
|
+
/** bind the module-global `emit`/`on` singleton (default true) */
|
|
343
|
+
global?: boolean;
|
|
344
|
+
/** called after a connection's client record is attached (seed `data`, …) */
|
|
345
|
+
onConnect?: (client: EventClient) => void;
|
|
346
|
+
/** called after a connection's client record is detached (cleanup) */
|
|
347
|
+
onDisconnect?: (client: EventClient) => void;
|
|
348
|
+
/** horizontal-scaling sync (server ⇄ server) */
|
|
349
|
+
cluster?: EventsClusterOptions;
|
|
350
|
+
/** offload-queue limits (the queue keeps cluster/state work off hot paths) */
|
|
351
|
+
queue?: { workers?: number; maxPending?: number };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface EventsClusterOptions {
|
|
355
|
+
/** stable id of this instance (default: random UUID) — self-dedupe */
|
|
356
|
+
instanceId?: string;
|
|
357
|
+
/** subject prefix (default: bindings subject prefix or "ignex") */
|
|
358
|
+
prefix?: string;
|
|
359
|
+
/**
|
|
360
|
+
* NATS-based cluster messaging. `true` reuses the server's NATS bridge
|
|
361
|
+
* connection (set `options.nats` too); pass `NatsBridgeOptions` to create a
|
|
362
|
+
* dedicated bridge; pass a `NatsBridge` to reuse an existing one.
|
|
363
|
+
*/
|
|
364
|
+
nats?: boolean | import("../bridge/nats").NatsBridgeOptions | import("../bridge/nats").NatsBridge;
|
|
365
|
+
/** Redis pub/sub cluster messaging (lazy `ioredis`; subjects mirror NATS) */
|
|
366
|
+
redis?: RedisConnectionOptions;
|
|
367
|
+
/** pluggable messaging transport (tests / custom brokers) */
|
|
368
|
+
transport?: ClusterTransport;
|
|
369
|
+
/**
|
|
370
|
+
* Shared-state store for presence / cluster group membership / client data.
|
|
371
|
+
* Default: per-instance memory. Production: `createRedisStateStore(...)`.
|
|
372
|
+
*/
|
|
373
|
+
state?: ClusterStateStore;
|
|
374
|
+
/** presence re-announce + prune cadence (ms, default 15_000) */
|
|
375
|
+
heartbeatMs?: number;
|
|
376
|
+
/** remote presence TTL (ms, default 60_000) */
|
|
377
|
+
presenceTtlMs?: number;
|
|
378
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// @generated
|
|
1
|
+
// @ts-nocheck — generated file: not subject to hand-typed strictness gates
|
|
2
|
+
// @generated by src/codegen/direct-gen.ts — DO NOT EDIT
|
|
2
3
|
import { encodeUtf8Into, ensureCapacity, utf8Len } from "../native/codec";
|
|
3
4
|
import { checkInt64 } from "../core/int64-guard";
|
|
4
5
|
import { anyEventNameToId, WIRE_VERSION, WIRE_HEADER_LEN } from "./registry";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// @generated
|
|
1
|
+
// @ts-nocheck — generated file: not subject to hand-typed strictness gates
|
|
2
|
+
// @generated by src/codegen/registry-gen.ts — DO NOT EDIT
|
|
2
3
|
import * as flatbuffers from "flatbuffers";
|
|
3
4
|
import { pooledByteBuffer } from "../transport/byte-buffer-pool";
|
|
4
5
|
|
|
@@ -46,6 +47,7 @@ import type { AnyEventName, ControlEventName, ControlEvents, Events, EventName }
|
|
|
46
47
|
|
|
47
48
|
export const WIRE_VERSION = 1;
|
|
48
49
|
export const WIRE_HEADER_LEN = 5; // [version:1][event_id:u32 LE]
|
|
50
|
+
export const SCHEMA_FINGERPRINT = 1656642724; // fnv1a32(canonical model)
|
|
49
51
|
|
|
50
52
|
export const eventNameToId: Record<EventName, number> = {
|
|
51
53
|
quote: 2995289047, // fnv1a32("quote")
|
package/src/generated/ts-ser.ts
CHANGED