@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,242 @@
|
|
|
1
|
+
import { WIRE_HEADER_LEN, WIRE_VERSION } from "./constants";
|
|
2
|
+
import { eventId } from "./hash";
|
|
3
|
+
import type { EmitContext, FieldDef, Model } from "./schema-model";
|
|
4
|
+
import { plainTsType, toSnake } from "./schema-model";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generated TS registry shared by BOTH sides:
|
|
8
|
+
* - server: `eventNameToId` for `encodeEvent`
|
|
9
|
+
* - client: `eventIdToDecoder` + `decodeFrame` for the standard `on(...)` API
|
|
10
|
+
*
|
|
11
|
+
* The `*ToPlain` adaptors convert flatc `unpack()` output (snake_case fields,
|
|
12
|
+
* numeric enums, bigint int64) into the exact `Events[K]` plain-object shape.
|
|
13
|
+
* Event-root adaptors are typed against `Events[K]` (compile-time check); nested
|
|
14
|
+
* adaptors infer their object shape.
|
|
15
|
+
*
|
|
16
|
+
* Event ids are STABLE FNV-1a 32-bit hashes of the event name (not insertion
|
|
17
|
+
* order), so schema reordering / field additions no longer renumber the wire
|
|
18
|
+
* format. The frame envelope is `[WIRE_VERSION:1][event_id:u32 LE][size-prefixed
|
|
19
|
+
* FlatBuffer]` — see `scripts/constants.ts`.
|
|
20
|
+
*
|
|
21
|
+
* `ctx.schemaImport === null` (user-mode generation) emits self-contained local
|
|
22
|
+
* `Events` / `ControlEvents` payload types instead of importing the app schema.
|
|
23
|
+
*/
|
|
24
|
+
export function emitRegistry(m: Model, fingerprint: number, ctx: EmitContext = {}): string {
|
|
25
|
+
const enumValues = new Map(m.enums.map((e) => [e.name, e.values]));
|
|
26
|
+
|
|
27
|
+
// tables reachable from an event root
|
|
28
|
+
const needed = new Set<string>();
|
|
29
|
+
const visit = (name: string) => {
|
|
30
|
+
if (needed.has(name)) return;
|
|
31
|
+
needed.add(name);
|
|
32
|
+
const t = m.tables.find((t) => t.name === name)!;
|
|
33
|
+
for (const f of t.fields) {
|
|
34
|
+
if (f.kind === "table" || f.kind === "vector-table") visit(f.tableName!);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
for (const ev of m.events) visit(ev.tableName);
|
|
38
|
+
const neededTables = m.tables.filter((t) => needed.has(t.name));
|
|
39
|
+
|
|
40
|
+
const userMode = ctx.schemaImport === null;
|
|
41
|
+
const schemaImport = userMode ? "" : (ctx.schemaImport ?? "../schema");
|
|
42
|
+
const poolImport = userMode
|
|
43
|
+
? (ctx.libraryImport ?? "@ignex/nova")
|
|
44
|
+
: "../transport/byte-buffer-pool";
|
|
45
|
+
|
|
46
|
+
const lines: string[] = [];
|
|
47
|
+
lines.push("// @ts-nocheck — generated file: not subject to hand-typed strictness gates");
|
|
48
|
+
lines.push("// @generated by src/codegen/registry-gen.ts — DO NOT EDIT");
|
|
49
|
+
lines.push('import * as flatbuffers from "flatbuffers";');
|
|
50
|
+
lines.push(`import { pooledByteBuffer } from ${JSON.stringify(poolImport)};`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push("import {");
|
|
53
|
+
for (const t of neededTables) {
|
|
54
|
+
lines.push(` ${t.name},`);
|
|
55
|
+
}
|
|
56
|
+
for (const t of neededTables) {
|
|
57
|
+
lines.push(` type ${t.name}T,`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('} from "./ts/backend";');
|
|
60
|
+
if (!userMode) {
|
|
61
|
+
lines.push(
|
|
62
|
+
`import type { AnyEventName, ControlEventName, ControlEvents, Events, EventName } from ${JSON.stringify(schemaImport)};`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(`export const WIRE_VERSION = ${WIRE_VERSION};`);
|
|
67
|
+
lines.push(`export const WIRE_HEADER_LEN = ${WIRE_HEADER_LEN}; // [version:1][event_id:u32 LE]`);
|
|
68
|
+
lines.push(`export const SCHEMA_FINGERPRINT = ${fingerprint}; // fnv1a32(canonical model)`);
|
|
69
|
+
lines.push("");
|
|
70
|
+
const appEvents = m.events.filter((e) => !e.control);
|
|
71
|
+
const controlEvents = m.events.filter((e) => e.control);
|
|
72
|
+
if (userMode) lines.push(...emitLocalTypes(m, neededTables, appEvents, controlEvents));
|
|
73
|
+
lines.push("export const eventNameToId: Record<EventName, number> = {");
|
|
74
|
+
for (const ev of appEvents)
|
|
75
|
+
lines.push(` ${ev.name}: ${eventId(ev.name)}, // fnv1a32("${ev.name}")`);
|
|
76
|
+
lines.push("};");
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push("export const controlEventNameToId: Record<ControlEventName, number> = {");
|
|
79
|
+
for (const ev of controlEvents)
|
|
80
|
+
lines.push(` ${ev.name}: ${eventId(ev.name)}, // fnv1a32("${ev.name}")`);
|
|
81
|
+
lines.push("};");
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push("/** merged app + control registry (used by encodeToScratch / JS encoder). */");
|
|
84
|
+
lines.push("export const anyEventNameToId: Record<string, number> = {");
|
|
85
|
+
for (const ev of m.events) lines.push(` ${ev.name}: ${eventId(ev.name)},`);
|
|
86
|
+
lines.push("};");
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("export const idToEventName: Record<number, EventName> = {");
|
|
89
|
+
for (const ev of appEvents) lines.push(` ${eventId(ev.name)}: ${JSON.stringify(ev.name)},`);
|
|
90
|
+
lines.push("};");
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push("/** merged app + control id → name lookup (decode dispatch). */");
|
|
93
|
+
lines.push("export const idToAnyEventName: Record<number, AnyEventName> = {");
|
|
94
|
+
for (const ev of m.events) lines.push(` ${eventId(ev.name)}: ${JSON.stringify(ev.name)},`);
|
|
95
|
+
lines.push("};");
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("export type DecodeFn = (bb: flatbuffers.ByteBuffer) => unknown;");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("export const eventIdToDecoder: Record<number, DecodeFn> = {");
|
|
100
|
+
for (const ev of m.events) {
|
|
101
|
+
const plain = toSnake(ev.tableName);
|
|
102
|
+
lines.push(
|
|
103
|
+
` ${eventId(ev.name)}: (bb) => ${plain}ToPlain(${ev.tableName}.getSizePrefixedRootAs${ev.tableName}(bb).unpack()),`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
lines.push("};");
|
|
107
|
+
lines.push("");
|
|
108
|
+
lines.push("const controlIds = new Set<number>([");
|
|
109
|
+
for (const ev of controlEvents) lines.push(` ${eventId(ev.name)},`);
|
|
110
|
+
lines.push("]);");
|
|
111
|
+
lines.push("export function isControlId(id: number): boolean { return controlIds.has(id); }");
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push(
|
|
114
|
+
"export function readFrameHeader(bytes: Uint8Array): { name: AnyEventName; id: number } | null {",
|
|
115
|
+
);
|
|
116
|
+
lines.push(" if (bytes.byteLength < WIRE_HEADER_LEN) return null;");
|
|
117
|
+
lines.push(" if (bytes[0] !== WIRE_VERSION) return null; // version mismatch — reject");
|
|
118
|
+
lines.push(
|
|
119
|
+
" const id = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(1, true);",
|
|
120
|
+
);
|
|
121
|
+
lines.push(" const name = idToAnyEventName[id];");
|
|
122
|
+
lines.push(" if (name === undefined) return null;");
|
|
123
|
+
lines.push(" return { name, id };");
|
|
124
|
+
lines.push("}");
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push("/** Decode a frame's payload given a validated id (no envelope re-parse). */");
|
|
127
|
+
lines.push("export function decodePayload(id: number, bytes: Uint8Array): unknown {");
|
|
128
|
+
lines.push(" const decoder = eventIdToDecoder[id];");
|
|
129
|
+
lines.push(" if (!decoder) return null;");
|
|
130
|
+
lines.push(" // Reuse the pooled ByteBuffer (no per-frame ByteBuffer + TextDecoder");
|
|
131
|
+
lines.push(" // allocation); position it at the payload start (relative to the view).");
|
|
132
|
+
lines.push(" return decoder(pooledByteBuffer(bytes, WIRE_HEADER_LEN));");
|
|
133
|
+
lines.push("}");
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push(
|
|
136
|
+
"export function decodeFrame(bytes: Uint8Array): { name: AnyEventName; id: number; payload: unknown } | null {",
|
|
137
|
+
);
|
|
138
|
+
lines.push(" const h = readFrameHeader(bytes);");
|
|
139
|
+
lines.push(" if (!h) return null;");
|
|
140
|
+
lines.push(" return { name: h.name, id: h.id, payload: decodePayload(h.id, bytes) };");
|
|
141
|
+
lines.push("}");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("// reused for every string decode (flatc object-API strings are Uint8Array)");
|
|
144
|
+
lines.push("const textDecoder = new TextDecoder();");
|
|
145
|
+
lines.push('function str(v: string | Uint8Array | null | undefined, dflt = ""): string {');
|
|
146
|
+
lines.push(' return v == null ? dflt : typeof v === "string" ? v : textDecoder.decode(v);');
|
|
147
|
+
lines.push("}");
|
|
148
|
+
lines.push("");
|
|
149
|
+
|
|
150
|
+
for (const t of neededTables) {
|
|
151
|
+
const event = m.events.find((e) => e.tableName === t.name);
|
|
152
|
+
const annot = event
|
|
153
|
+
? `: ${event.control ? "ControlEvents" : "Events"}[${JSON.stringify(event.name)}]`
|
|
154
|
+
: "";
|
|
155
|
+
lines.push(`function ${toSnake(t.name)}ToPlain(o: ${t.name}T)${annot} {`);
|
|
156
|
+
lines.push(" return {");
|
|
157
|
+
for (const f of t.fields) {
|
|
158
|
+
lines.push(` ${f.jsonName}: ${plainExpr(f, enumValues)},`);
|
|
159
|
+
}
|
|
160
|
+
lines.push(" };");
|
|
161
|
+
lines.push("}");
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Self-contained `Events` / `ControlEvents` payload types (user-mode codegen). */
|
|
169
|
+
function emitLocalTypes(
|
|
170
|
+
m: Model,
|
|
171
|
+
tables: Model["tables"],
|
|
172
|
+
appEvents: Model["events"],
|
|
173
|
+
controlEvents: Model["events"],
|
|
174
|
+
): string[] {
|
|
175
|
+
const lines: string[] = [];
|
|
176
|
+
for (const t of tables) {
|
|
177
|
+
lines.push(`export interface ${t.name}Payload {`);
|
|
178
|
+
for (const f of t.fields) {
|
|
179
|
+
lines.push(` ${f.jsonName}${f.required ? "" : "?"}: ${plainTsType(m, f)};`);
|
|
180
|
+
}
|
|
181
|
+
lines.push("}");
|
|
182
|
+
lines.push("");
|
|
183
|
+
}
|
|
184
|
+
const appNames = appEvents.map((e) => `"${e.name}"`).join(" | ");
|
|
185
|
+
const ctlNames = controlEvents.map((e) => `"${e.name}"`).join(" | ");
|
|
186
|
+
lines.push(`export type EventName = ${appNames};`);
|
|
187
|
+
lines.push(`export type ControlEventName = ${ctlNames};`);
|
|
188
|
+
lines.push(`export type AnyEventName = EventName | ControlEventName;`);
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("export type Events = {");
|
|
191
|
+
for (const ev of appEvents) lines.push(` ${JSON.stringify(ev.name)}: ${ev.tableName}Payload;`);
|
|
192
|
+
lines.push("};");
|
|
193
|
+
lines.push("export type ControlEvents = {");
|
|
194
|
+
for (const ev of controlEvents)
|
|
195
|
+
lines.push(` ${JSON.stringify(ev.name)}: ${ev.tableName}Payload;`);
|
|
196
|
+
lines.push("};");
|
|
197
|
+
lines.push("");
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function plainExpr(f: FieldDef, enumValues: Map<string, string[]>): string {
|
|
202
|
+
// flatc TS accessors/.T fields are camelCase (it converts the snake_case .fbs
|
|
203
|
+
// names) — so read via jsonName, which equals the TypeBox/Events key.
|
|
204
|
+
const o = `o.${f.jsonName}`;
|
|
205
|
+
switch (f.kind) {
|
|
206
|
+
case "string":
|
|
207
|
+
return f.required ? `str(${o})` : `${o} == null ? undefined : str(${o})`;
|
|
208
|
+
case "double":
|
|
209
|
+
case "bool":
|
|
210
|
+
return o;
|
|
211
|
+
case "int64":
|
|
212
|
+
// bigint-annotated fields stay bigint (exact); plain int64 → number
|
|
213
|
+
return f.bigint ? o : `Number(${o})`;
|
|
214
|
+
case "enum":
|
|
215
|
+
return `${enumTuple(f.enumName!, enumValues)}[${o}] ?? ${JSON.stringify(enumValues.get(f.enumName!)?.[0] ?? "")}`;
|
|
216
|
+
case "table":
|
|
217
|
+
// Required nested tables are always written by the encoder, so the
|
|
218
|
+
// decoder may assert non-null (keeps the Events[K] type honest);
|
|
219
|
+
// optional tables keep the defensive undefined fallback.
|
|
220
|
+
return f.required
|
|
221
|
+
? `${toSnake(f.tableName!)}ToPlain(${o}!)`
|
|
222
|
+
: `${o} === null || ${o} === undefined ? undefined : ${toSnake(f.tableName!)}ToPlain(${o})`;
|
|
223
|
+
case "vector-string":
|
|
224
|
+
return `${o}.map((s) => str(s))`;
|
|
225
|
+
case "vector-double":
|
|
226
|
+
case "vector-bool":
|
|
227
|
+
return `${o}.map((n) => n)`;
|
|
228
|
+
case "vector-int64":
|
|
229
|
+
return `${o}.map((n) => Number(n))`;
|
|
230
|
+
case "vector-enum":
|
|
231
|
+
return `${o}.map((n) => ${enumTuple(f.enumName!, enumValues)}[n] ?? ${JSON.stringify(enumValues.get(f.enumName!)?.[0] ?? "")})`;
|
|
232
|
+
case "vector-table":
|
|
233
|
+
return `${o}.map((c) => ${toSnake(f.tableName!)}ToPlain(c))`;
|
|
234
|
+
default:
|
|
235
|
+
return o;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function enumTuple(enumName: string, enumValues: Map<string, string[]>): string {
|
|
240
|
+
const values = enumValues.get(enumName) ?? [];
|
|
241
|
+
return `([${values.map((v) => JSON.stringify(v)).join(", ")}] as const)`;
|
|
242
|
+
}
|