@ignex/nova 0.1.1 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +136 -33
  2. package/docs/ai/LOCAL_DEV.md +81 -0
  3. package/docs/ai/TREE.md +292 -0
  4. package/docs/architecture.md +101 -28
  5. package/docs/events.md +252 -0
  6. package/docs/generic-bindings.md +207 -0
  7. package/docs/publishing.md +2 -2
  8. package/docs/wire-format.md +74 -20
  9. package/index.ts +75 -27
  10. package/package.json +13 -2
  11. package/prebuilds/linux-x64/libignex_ffi.so +0 -0
  12. package/public/bindings.ts +24 -0
  13. package/public/client.ts +5 -1
  14. package/public/events.ts +71 -0
  15. package/public/generate.ts +510 -0
  16. package/public/internal.ts +16 -0
  17. package/public/nats.ts +9 -5
  18. package/public/server.ts +52 -16
  19. package/rust/src/ffi.rs +10 -0
  20. package/rust/src/generated/backend.rs +503 -0
  21. package/rust/src/transcode/generated.rs +377 -17
  22. package/src/bindings/assemble.ts +73 -0
  23. package/src/bindings/default.ts +65 -0
  24. package/src/bindings/types.ts +113 -0
  25. package/src/bridge/nats/inbound.ts +46 -0
  26. package/src/bridge/nats/index.ts +131 -0
  27. package/src/bridge/nats/real-transport.ts +133 -0
  28. package/src/bridge/nats/types.ts +80 -0
  29. package/src/bridge/subjects.ts +3 -0
  30. package/src/codegen/constants.ts +28 -0
  31. package/src/codegen/direct-gen.ts +564 -0
  32. package/src/codegen/fingerprint.ts +44 -0
  33. package/src/codegen/hash.ts +25 -0
  34. package/src/codegen/registry-gen.ts +246 -0
  35. package/src/codegen/rust-glue-gen.ts +552 -0
  36. package/src/codegen/schema-model.ts +363 -0
  37. package/src/codegen/ts-ser-gen.ts +230 -0
  38. package/src/codegen/typebox-to-fbs.ts +60 -0
  39. package/src/core/auth.ts +67 -5
  40. package/src/core/client-heartbeat.ts +2 -1
  41. package/src/core/client-reconnect.ts +9 -2
  42. package/src/core/client-rpc.ts +75 -0
  43. package/src/core/client-state.ts +63 -8
  44. package/src/core/client-wire.ts +148 -15
  45. package/src/core/client.ts +105 -31
  46. package/src/core/groups.ts +8 -0
  47. package/src/core/metrics.ts +42 -21
  48. package/src/core/outbound.ts +62 -11
  49. package/src/core/rate-limit.ts +69 -0
  50. package/src/core/replay.ts +41 -1
  51. package/src/core/resume.ts +181 -0
  52. package/src/core/rooms.ts +10 -3
  53. package/src/core/routing.ts +144 -12
  54. package/src/core/server/client-info.ts +37 -0
  55. package/src/core/server/http-routes.ts +59 -0
  56. package/src/core/server/index.ts +360 -0
  57. package/src/core/server/metrics-view.ts +53 -0
  58. package/src/core/server/socket-lifecycle.ts +57 -0
  59. package/src/core/state.ts +124 -14
  60. package/src/core/topic-log.ts +86 -0
  61. package/src/events/clients.ts +174 -0
  62. package/src/events/cluster/dedupe.ts +43 -0
  63. package/src/events/cluster/envelope.ts +149 -0
  64. package/src/events/cluster/index.ts +50 -0
  65. package/src/events/cluster/keys.ts +33 -0
  66. package/src/events/cluster/kinds.ts +32 -0
  67. package/src/events/cluster/presence-table.ts +99 -0
  68. package/src/events/cluster/presence.ts +53 -0
  69. package/src/events/cluster/redis-client.ts +50 -0
  70. package/src/events/cluster/store-memory.ts +67 -0
  71. package/src/events/cluster/store-redis.ts +44 -0
  72. package/src/events/cluster/subjects.ts +30 -0
  73. package/src/events/cluster/sync.ts +476 -0
  74. package/src/events/cluster/transport-nats.ts +24 -0
  75. package/src/events/cluster/transport-redis.ts +120 -0
  76. package/src/events/cluster-rpc.ts +196 -0
  77. package/src/events/data.ts +38 -0
  78. package/src/events/delivery.ts +83 -0
  79. package/src/events/emit.ts +173 -0
  80. package/src/events/global.ts +117 -0
  81. package/src/events/groups.ts +118 -0
  82. package/src/events/hub/context-factory.ts +79 -0
  83. package/src/events/hub/dispatch.ts +86 -0
  84. package/src/events/hub/index.ts +536 -0
  85. package/src/events/hub/internal.ts +31 -0
  86. package/src/events/hub/metrics-snapshot.ts +84 -0
  87. package/src/events/hub/resolve-cluster.ts +49 -0
  88. package/src/events/index.ts +61 -0
  89. package/src/events/queue.ts +123 -0
  90. package/src/events/registry.ts +214 -0
  91. package/src/events/schedule.ts +73 -0
  92. package/src/events/trace.ts +283 -0
  93. package/src/events/types/client.ts +68 -0
  94. package/src/events/types/cluster.ts +40 -0
  95. package/src/events/types/context.ts +50 -0
  96. package/src/events/types/emit-target.ts +29 -0
  97. package/src/events/types/groups.ts +35 -0
  98. package/src/events/types/hub.ts +124 -0
  99. package/src/events/types/index.ts +30 -0
  100. package/src/events/types/metrics.ts +52 -0
  101. package/src/events/types/options.ts +62 -0
  102. package/src/generated/direct-ser.ts +148 -60
  103. package/src/generated/fbs/backend.fbs +24 -1
  104. package/src/generated/registry.ts +94 -33
  105. package/src/generated/rust/backend_generated.rs +503 -0
  106. package/src/generated/ts/backend.ts +4 -0
  107. package/src/generated/ts/resume.ts +74 -0
  108. package/src/generated/ts/resumed.ts +88 -0
  109. package/src/generated/ts/rpc-call.ts +112 -0
  110. package/src/generated/ts/rpc-result.ts +126 -0
  111. package/src/generated/ts/snapshot-request.ts +19 -5
  112. package/src/generated/ts-ser.ts +110 -17
  113. package/src/generated/wire-registry.json +7 -2
  114. package/src/native/ffi.ts +85 -28
  115. package/src/schema/index.ts +49 -2
  116. package/src/server.ts +7 -3
  117. package/src/transport/transport.ts +200 -79
  118. package/src/bridge/nats.ts +0 -269
  119. package/src/core/server.ts +0 -294
  120. package/src/transport/stats.ts +0 -44
@@ -0,0 +1,552 @@
1
+ import { WIRE_HEADER_LEN, WIRE_VERSION } from "./constants";
2
+ import { eventId } from "./hash";
3
+ import type { EventDef, FieldDef, Model } from "./schema-model";
4
+ import { isDirectableEvent, toSnake } from "./schema-model";
5
+
6
+ function rustTypeOf(f: FieldDef): string {
7
+ switch (f.kind) {
8
+ case "string":
9
+ return f.required ? "String" : "Option<String>";
10
+ case "double":
11
+ return "f64";
12
+ case "int64":
13
+ return "i64";
14
+ case "bool":
15
+ return "bool";
16
+ case "enum":
17
+ return "Option<String>";
18
+ case "table":
19
+ return `Option<${f.tableName}Json>`;
20
+ case "vector-string":
21
+ return "Vec<String>";
22
+ case "vector-double":
23
+ return "Vec<f64>";
24
+ case "vector-int64":
25
+ return "Vec<i64>";
26
+ case "vector-bool":
27
+ return "Vec<bool>";
28
+ case "vector-enum":
29
+ return "Vec<String>";
30
+ case "vector-table":
31
+ return `Vec<${f.tableName}Json>`;
32
+ }
33
+ }
34
+
35
+ function serdeAttr(f: FieldDef): string {
36
+ return f.kind === "int64"
37
+ ? '#[serde(default, deserialize_with = "de_i64")]'
38
+ : "#[serde(default)]";
39
+ }
40
+
41
+ /** Statements that build any needed offsets before `Xxx::create(...)`. */
42
+ function buildFieldExprs(f: FieldDef): string {
43
+ const fb = f.fbName;
44
+ switch (f.kind) {
45
+ case "string":
46
+ return f.required
47
+ ? `let ${fb} = fbb.create_string(&j.${fb});`
48
+ : `let ${fb} = j.${fb}.as_deref().map(|s| fbb.create_string(s));`;
49
+ case "double":
50
+ case "int64":
51
+ case "bool":
52
+ return "";
53
+ case "enum":
54
+ return `let ${fb} = j.${fb}.as_deref().map(${toSnake(f.enumName!)}_from_str).unwrap_or_default();`;
55
+ case "table":
56
+ return `let ${fb} = j.${fb}.as_ref().map(|c| build_${toSnake(f.tableName!)}(fbb, c));`;
57
+ case "vector-string":
58
+ return (
59
+ `let ${fb} = if j.${fb}.is_empty() { None } else { ` +
60
+ `let offs: Vec<flatbuffers::WIPOffset<&str>> = j.${fb}.iter().map(|s| fbb.create_string(s)).collect(); ` +
61
+ `Some(fbb.create_vector(&offs)) };`
62
+ );
63
+ case "vector-double":
64
+ case "vector-int64":
65
+ case "vector-bool":
66
+ return `let ${fb} = if j.${fb}.is_empty() { None } else { Some(fbb.create_vector(&j.${fb})) };`;
67
+ case "vector-enum":
68
+ return (
69
+ `let ${fb} = if j.${fb}.is_empty() { None } else { ` +
70
+ `let vals: Vec<backend::${f.enumName}> = j.${fb}.iter().map(|s| ${toSnake(f.enumName!)}_from_str(s)).collect(); ` +
71
+ `Some(fbb.create_vector(&vals)) };`
72
+ );
73
+ case "vector-table":
74
+ return (
75
+ `let ${fb} = if j.${fb}.is_empty() { None } else { ` +
76
+ `let offs: Vec<flatbuffers::WIPOffset<backend::${f.tableName}<'a>>> = ` +
77
+ `j.${fb}.iter().map(|c| build_${toSnake(f.tableName!)}(fbb, c)).collect(); ` +
78
+ `Some(fbb.create_vector(&offs)) };`
79
+ );
80
+ }
81
+ }
82
+
83
+ function argsExpr(f: FieldDef): string {
84
+ const fb = f.fbName;
85
+ switch (f.kind) {
86
+ case "string":
87
+ return f.required ? `${fb}: Some(${fb}),` : `${fb},`;
88
+ case "double":
89
+ case "int64":
90
+ case "bool":
91
+ return `${fb}: j.${fb},`;
92
+ default:
93
+ return `${fb},`;
94
+ }
95
+ }
96
+
97
+ function emitJsonStruct(f: FieldDef): string {
98
+ const ty = rustTypeOf(f);
99
+ return ` ${serdeAttr(f)}\n pub ${f.fbName}: ${ty},`;
100
+ }
101
+
102
+ function emitBuildFn(_m: Model, t: { name: string; fields: FieldDef[] }): string {
103
+ const snake = toSnake(t.name);
104
+ const exprs = t.fields
105
+ .map(buildFieldExprs)
106
+ .filter((s) => s.length > 0)
107
+ .join("\n ");
108
+ const args = t.fields.map(argsExpr).join("\n ");
109
+ const lines = [
110
+ `pub fn build_${snake}<'a>(fbb: &mut flatbuffers::FlatBufferBuilder<'a>, j: &${t.name}Json) -> flatbuffers::WIPOffset<backend::${t.name}<'a>> {`,
111
+ ` ${exprs}`,
112
+ ` backend::${t.name}::create(fbb, &backend::${t.name}Args {`,
113
+ ` ${args}`,
114
+ ` ..Default::default()`,
115
+ ` })`,
116
+ `}`,
117
+ ];
118
+ return lines.join("\n");
119
+ }
120
+
121
+ function emitSerializeFn(_m: Model, ev: { name: string; tableName: string }): string {
122
+ const snake = toSnake(ev.name);
123
+ const lines = [
124
+ `pub fn serialize_${snake}(json: &[u8], out: &mut [u8]) -> Result<usize, TranscodeError> {`,
125
+ ` let j: ${ev.tableName}Json = serde_json::from_slice(json).map_err(|_| TranscodeError::Parse)?;`,
126
+ ` copy_finished(out, |fbb| build_${toSnake(ev.tableName)}(fbb, &j))`,
127
+ `}`,
128
+ ];
129
+ return lines.join("\n");
130
+ }
131
+
132
+ /**
133
+ * Direct-args helpers: emit an `extern "C"` fn per flat event that takes the
134
+ * JS object's fields directly (strings as cstring, scalars as numbers, enums as
135
+ * u32 index) — no JSON.stringify / serde_json on the hot path.
136
+ */
137
+ function directArgDecl(f: FieldDef): string {
138
+ switch (f.kind) {
139
+ case "string":
140
+ // cstring ARG — the engine transcodes the JS string in-engine (zero JS
141
+ // encode); Rust borrows via CStr::from_ptr, no length arg, no from_utf8
142
+ // validation (the caller's string is always valid UTF-8).
143
+ return `${f.fbName}: *const c_char,`;
144
+ case "double":
145
+ return `${f.fbName}: f64,`;
146
+ case "int64":
147
+ return `${f.fbName}: i64,`;
148
+ case "bool":
149
+ return `${f.fbName}: u8,`;
150
+ case "enum":
151
+ return `${f.fbName}: u32,`;
152
+ case "vector-table":
153
+ case "vector-string":
154
+ case "vector-double":
155
+ case "vector-int64":
156
+ case "vector-bool":
157
+ case "vector-enum":
158
+ // packed binary blob: (ptr, len)
159
+ return `${f.fbName}_ptr: *const u8,\n ${f.fbName}_len: usize,`;
160
+ default:
161
+ throw new Error(`direct-args: unsupported field kind ${f.kind}`);
162
+ }
163
+ }
164
+
165
+ function directDecodeLines(f: FieldDef): string {
166
+ const fb = f.fbName;
167
+ switch (f.kind) {
168
+ case "string":
169
+ // cstring arg: null → empty/None; else borrow without UTF-8 validation
170
+ // (`from_utf8_unchecked` — the engine transcoded valid UTF-8).
171
+ if (f.required) {
172
+ return `let ${fb} = if ${fb}.is_null() { "" } else { std::str::from_utf8_unchecked(CStr::from_ptr(${fb}).to_bytes()) };`;
173
+ }
174
+ return `let ${fb} = if ${fb}.is_null() { None } else { Some(std::str::from_utf8_unchecked(CStr::from_ptr(${fb}).to_bytes())) };`;
175
+ case "enum":
176
+ return `let ${fb} = backend::${f.enumName}(${fb} as i32);`;
177
+ default:
178
+ return "";
179
+ }
180
+ }
181
+
182
+ function directBuildLines(f: FieldDef, parent: string): string {
183
+ const fb = f.fbName;
184
+ switch (f.kind) {
185
+ case "string":
186
+ return f.required
187
+ ? `let ${fb} = fbb.create_string(${fb});`
188
+ : `let ${fb} = ${fb}.map(|s| fbb.create_string(s));`;
189
+ case "vector-table":
190
+ case "vector-string":
191
+ case "vector-double":
192
+ case "vector-int64":
193
+ case "vector-bool":
194
+ case "vector-enum":
195
+ return `let ${fb} = read_packed_${toSnake(parent)}_${fb}(fbb, ${fb}_ptr, ${fb}_len);`;
196
+ default:
197
+ return "";
198
+ }
199
+ }
200
+
201
+ function directArgAssign(f: FieldDef): string {
202
+ const fb = f.fbName;
203
+ switch (f.kind) {
204
+ case "string":
205
+ return f.required ? `${fb}: Some(${fb}),` : `${fb},`;
206
+ case "double":
207
+ case "int64":
208
+ return `${fb}: ${fb},`;
209
+ case "bool":
210
+ return `${fb}: ${fb} != 0,`;
211
+ case "enum":
212
+ case "vector-table":
213
+ case "vector-string":
214
+ case "vector-double":
215
+ case "vector-int64":
216
+ case "vector-bool":
217
+ case "vector-enum":
218
+ return `${fb},`;
219
+ default:
220
+ throw new Error(`direct-args: unsupported field kind ${f.kind}`);
221
+ }
222
+ }
223
+
224
+ /** Packed-vector readers: decode a JS-packed binary blob into a FlatBuffer vector. */
225
+ function isVectorKind(kind: string): boolean {
226
+ return [
227
+ "vector-table",
228
+ "vector-string",
229
+ "vector-double",
230
+ "vector-int64",
231
+ "vector-bool",
232
+ "vector-enum",
233
+ ].includes(kind);
234
+ }
235
+
236
+ function emitPackedElemRead(ef: FieldDef): string {
237
+ const fb = ef.fbName;
238
+ switch (ef.kind) {
239
+ case "string":
240
+ return `let ${fb}_len = read_u32(b, &mut off) as usize; let ${fb} = std::str::from_utf8(&b[off..off + ${fb}_len]).unwrap_or(""); off += ${fb}_len; let ${fb}_off = fbb.create_string(${fb});`;
241
+ case "double":
242
+ return `let ${fb} = read_f64(b, &mut off);`;
243
+ case "int64":
244
+ return `let ${fb} = read_i64(b, &mut off);`;
245
+ case "bool":
246
+ return `let ${fb} = b[off] != 0; off += 1;`;
247
+ case "enum":
248
+ return `let ${fb} = backend::${ef.enumName}(read_u32(b, &mut off) as i32);`;
249
+ default:
250
+ throw new Error(`packed element: unsupported field kind ${ef.kind}`);
251
+ }
252
+ }
253
+
254
+ function emitPackedElemAssign(ef: FieldDef): string {
255
+ const fb = ef.fbName;
256
+ switch (ef.kind) {
257
+ case "string":
258
+ return `${fb}: Some(${fb}_off),`;
259
+ case "double":
260
+ case "int64":
261
+ case "bool":
262
+ case "enum":
263
+ return `${fb},`;
264
+ default:
265
+ throw new Error(`packed element: unsupported field kind ${ef.kind}`);
266
+ }
267
+ }
268
+
269
+ function emitPackedReader(m: Model, parent: string, f: FieldDef): string {
270
+ const name = `read_packed_${toSnake(parent)}_${f.fbName}`;
271
+ const lines: string[] = [];
272
+ if (f.kind === "vector-table") {
273
+ const elem = m.tables.find((e) => e.name === f.tableName)!;
274
+ lines.push(
275
+ `fn ${name}<'a>(fbb: &mut flatbuffers::FlatBufferBuilder<'a>, ptr: *const u8, len: usize) -> Option<flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<backend::${f.tableName}<'a>>>>> {`,
276
+ );
277
+ lines.push(` if ptr.is_null() || len == 0 { return None; }`);
278
+ lines.push(` let b = unsafe { std::slice::from_raw_parts(ptr, len) };`);
279
+ lines.push(` let mut off = 0usize;`);
280
+ lines.push(` let count = read_u32(b, &mut off) as usize;`);
281
+ lines.push(
282
+ ` let mut offsets: Vec<flatbuffers::WIPOffset<backend::${f.tableName}<'a>>> = Vec::with_capacity(count);`,
283
+ );
284
+ lines.push(` for _ in 0..count {`);
285
+ for (const ef of elem.fields) lines.push(` ${emitPackedElemRead(ef)}`);
286
+ lines.push(
287
+ ` let item = backend::${f.tableName}::create(fbb, &backend::${f.tableName}Args {`,
288
+ );
289
+ for (const ef of elem.fields) lines.push(` ${emitPackedElemAssign(ef)}`);
290
+ lines.push(` ..Default::default()`);
291
+ lines.push(` });`);
292
+ lines.push(` offsets.push(item);`);
293
+ lines.push(` }`);
294
+ lines.push(` Some(fbb.create_vector(&offsets))`);
295
+ lines.push(`}`);
296
+ return lines.join("\n");
297
+ }
298
+ const elemTy = (
299
+ {
300
+ "vector-double": "f64",
301
+ "vector-int64": "i64",
302
+ "vector-bool": "bool",
303
+ "vector-string": "flatbuffers::ForwardsUOffset<&'a str>",
304
+ "vector-enum": `backend::${f.enumName}`,
305
+ } as Record<string, string>
306
+ )[f.kind]!;
307
+ const vecInner = f.kind === "vector-string" ? "flatbuffers::WIPOffset<&'a str>" : elemTy;
308
+ lines.push(
309
+ `fn ${name}<'a>(fbb: &mut flatbuffers::FlatBufferBuilder<'a>, ptr: *const u8, len: usize) -> Option<flatbuffers::WIPOffset<flatbuffers::Vector<'a, ${elemTy}>>> {`,
310
+ );
311
+ lines.push(` if ptr.is_null() || len == 0 { return None; }`);
312
+ lines.push(` let b = unsafe { std::slice::from_raw_parts(ptr, len) };`);
313
+ lines.push(` let mut off = 0usize;`);
314
+ lines.push(` let count = read_u32(b, &mut off) as usize;`);
315
+ lines.push(` let mut vals: Vec<${vecInner}> = Vec::with_capacity(count);`);
316
+ lines.push(` for _ in 0..count {`);
317
+ if (f.kind === "vector-double") lines.push(` vals.push(read_f64(b, &mut off));`);
318
+ else if (f.kind === "vector-int64") lines.push(` vals.push(read_i64(b, &mut off));`);
319
+ else if (f.kind === "vector-bool") lines.push(` vals.push(b[off] != 0); off += 1;`);
320
+ else if (f.kind === "vector-string")
321
+ lines.push(
322
+ ` let l = read_u32(b, &mut off) as usize; let s = std::str::from_utf8(&b[off..off + l]).unwrap_or(""); off += l; vals.push(fbb.create_string(s));`,
323
+ );
324
+ else if (f.kind === "vector-enum")
325
+ lines.push(` vals.push(backend::${f.enumName}(read_u32(b, &mut off) as i32));`);
326
+ lines.push(` }`);
327
+ lines.push(` Some(fbb.create_vector(&vals))`);
328
+ lines.push(`}`);
329
+ return lines.join("\n");
330
+ }
331
+
332
+ function emitPackedReaders(m: Model): string[] {
333
+ const directEvents = m.events.filter((ev) => isDirectableEvent(m, ev));
334
+ const vectorFields: { parent: string; f: FieldDef }[] = [];
335
+ for (const ev of directEvents) {
336
+ const table = m.tables.find((t) => t.name === ev.tableName)!;
337
+ for (const f of table.fields)
338
+ if (isVectorKind(f.kind)) vectorFields.push({ parent: table.name, f });
339
+ }
340
+ if (vectorFields.length === 0) return [];
341
+ const lines: string[] = [
342
+ "fn read_u32(b: &[u8], off: &mut usize) -> u32 {",
343
+ " let v = u32::from_le_bytes([b[*off], b[*off + 1], b[*off + 2], b[*off + 3]]);",
344
+ " *off += 4;",
345
+ " v",
346
+ "}",
347
+ "",
348
+ "fn read_i64(b: &[u8], off: &mut usize) -> i64 {",
349
+ " let mut buf = [0u8; 8];",
350
+ " buf.copy_from_slice(&b[*off..*off + 8]);",
351
+ " *off += 8;",
352
+ " i64::from_le_bytes(buf)",
353
+ "}",
354
+ "",
355
+ "fn read_f64(b: &[u8], off: &mut usize) -> f64 {",
356
+ " f64::from_bits(read_i64(b, off) as u64)",
357
+ "}",
358
+ "",
359
+ ];
360
+ for (const { parent, f } of vectorFields) {
361
+ lines.push(emitPackedReader(m, parent, f));
362
+ lines.push("");
363
+ }
364
+ return lines;
365
+ }
366
+
367
+ function emitDirectFn(m: Model, ev: EventDef): string {
368
+ const table = m.tables.find((t) => t.name === ev.tableName)!;
369
+ const id = eventId(ev.name);
370
+ const snake = toSnake(ev.name);
371
+ // Dotted event names ("chat.send") must not leak into the Rust fn
372
+ // identifier — keep the C-ABI symbol exact via #[export_name] instead.
373
+ const fnId = snake.replace(/[^a-zA-Z0-9_]/g, "_");
374
+ const decls = table.fields.map(directArgDecl).join("\n ");
375
+ const decodes = table.fields
376
+ .map(directDecodeLines)
377
+ .filter((s) => s.length > 0)
378
+ .join("\n ");
379
+ const builds = table.fields
380
+ .map((f) => directBuildLines(f, table.name))
381
+ .filter((s) => s.length > 0)
382
+ .join("\n ");
383
+ const assigns = table.fields.map(directArgAssign).join("\n ");
384
+ return [
385
+ `#[export_name = "fb_${snake}_serialize"]`,
386
+ `pub unsafe extern "C" fn fb_${fnId}_serialize(`,
387
+ ` ${decls}`,
388
+ ` out: *mut u8,`,
389
+ ` out_cap: usize,`,
390
+ `) -> usize {`,
391
+ ` std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {`,
392
+ ` if out.is_null() && out_cap != 0 {`,
393
+ ` return 0;`,
394
+ ` }`,
395
+ ` ${decodes}`,
396
+ ` // envelope = [WIRE_VERSION:1][event_id:u32 LE]; flatbuffer payload follows`,
397
+ ` let payload: &mut [u8] = if out_cap < WIRE_HEADER_LEN {`,
398
+ ` &mut []`,
399
+ ` } else {`,
400
+ ` std::slice::from_raw_parts_mut(out.add(WIRE_HEADER_LEN), out_cap - WIRE_HEADER_LEN)`,
401
+ ` };`,
402
+ ` match copy_finished(payload, |fbb| {`,
403
+ ` ${builds}`,
404
+ ` backend::${ev.tableName}::create(fbb, &backend::${ev.tableName}Args {`,
405
+ ` ${assigns}`,
406
+ ` ..Default::default()`,
407
+ ` })`,
408
+ ` }) {`,
409
+ ` Ok(written) => {`,
410
+ ` let needed = written + WIRE_HEADER_LEN;`,
411
+ ` if needed <= out_cap {`,
412
+ ` *out.add(0) = WIRE_VERSION;`,
413
+ ` let id_bytes = ${id}u32.to_le_bytes();`,
414
+ ` std::ptr::copy_nonoverlapping(id_bytes.as_ptr(), out.add(1), 4);`,
415
+ ` // [flags:1][seq:u64 LE] — pristine (flags=0, seq=0); the`,
416
+ ` // server stamps per-destination delivery seqs on send.`,
417
+ ` *out.add(5) = 0;`,
418
+ ` std::ptr::write_bytes(out.add(6), 0, 8);`,
419
+ ` }`,
420
+ ` needed`,
421
+ ` }`,
422
+ ` Err(_) => 0,`,
423
+ ` }`,
424
+ ` }))`,
425
+ ` .unwrap_or(0)`,
426
+ `}`,
427
+ ].join("\n");
428
+ }
429
+
430
+ /**
431
+ * TypeBox model → Rust JSON→FlatBuffer glue. Emits, per table: a serde JSON
432
+ * struct + a `build_*` fn that calls the flatc-generated `backend::<T>::create`
433
+ * with `XxxArgs`. Per event: a `serialize_<event>` that finishes a
434
+ * size-prefixed buffer. Plus a shared `serialize_event(event_id, ...)` dispatcher
435
+ * used by the C-ABI `fb_serialize` export. Event ids are STABLE FNV-1a 32-bit
436
+ * hashes of the event name, matching `generated/registry.ts`. The frame
437
+ * envelope `[WIRE_VERSION][event_id:u32]` is written by the C-ABI layer.
438
+ */
439
+ export function emitRustGlue(m: Model, fingerprint: number): string {
440
+ const lines: string[] = [];
441
+ lines.push("// @generated by src/codegen/rust-glue-gen.ts — DO NOT EDIT");
442
+ lines.push("#![allow(clippy::all)]");
443
+ lines.push("#![allow(dead_code)]");
444
+ lines.push("#![allow(unused_imports)]");
445
+ lines.push("");
446
+ lines.push("use std::cell::RefCell;");
447
+ lines.push("use std::ffi::CStr;");
448
+ lines.push("use std::os::raw::c_char;");
449
+ lines.push("use serde::Deserialize;");
450
+ lines.push("use crate::generated::backend;");
451
+ lines.push("");
452
+ lines.push(`pub const WIRE_VERSION: u8 = ${WIRE_VERSION};`);
453
+ lines.push(
454
+ `pub const WIRE_HEADER_LEN: usize = ${WIRE_HEADER_LEN}; // [version:1][event_id:u32 LE][flags:1][seq:u64 LE]`,
455
+ );
456
+ lines.push(`pub const SCHEMA_FINGERPRINT: u64 = ${fingerprint}; // fnv1a32(canonical model)`);
457
+ lines.push("");
458
+ lines.push("thread_local! {");
459
+ lines.push(" static FBB: RefCell<flatbuffers::FlatBufferBuilder<'static>> =");
460
+ lines.push(" RefCell::new(flatbuffers::FlatBufferBuilder::new());");
461
+ lines.push("}");
462
+ lines.push("");
463
+ lines.push("fn copy_finished<T>(");
464
+ lines.push(" out: &mut [u8],");
465
+ lines.push(
466
+ " build: impl FnOnce(&mut flatbuffers::FlatBufferBuilder<'static>) -> flatbuffers::WIPOffset<T>,",
467
+ );
468
+ lines.push(") -> Result<usize, TranscodeError> {");
469
+ lines.push(" FBB.with(|cell| {");
470
+ lines.push(" let mut fbb = cell.borrow_mut();");
471
+ lines.push(" fbb.reset();");
472
+ lines.push(" let root = build(&mut fbb);");
473
+ lines.push(' fbb.finish_size_prefixed(root, Some("IGNX"));');
474
+ lines.push(" let data = fbb.finished_data();");
475
+ lines.push(" if data.len() > out.len() {");
476
+ lines.push(" return Ok(data.len());");
477
+ lines.push(" }");
478
+ lines.push(" out[..data.len()].copy_from_slice(data);");
479
+ lines.push(" Ok(data.len())");
480
+ lines.push(" })");
481
+ lines.push("}");
482
+ lines.push("");
483
+ lines.push("#[derive(Debug)]");
484
+ lines.push("pub enum TranscodeError {");
485
+ lines.push(" Parse,");
486
+ lines.push(" UnknownEvent,");
487
+ lines.push("}");
488
+ lines.push("");
489
+ lines.push("fn de_i64<'de, D>(d: D) -> Result<i64, D::Error>");
490
+ lines.push("where");
491
+ lines.push(" D: serde::Deserializer<'de>,");
492
+ lines.push("{");
493
+ lines.push(" let v = serde_json::Value::deserialize(d)?;");
494
+ lines.push(" Ok(v.as_i64().or_else(|| v.as_f64().map(|f| f as i64)).unwrap_or(0))");
495
+ lines.push("}");
496
+ lines.push("");
497
+
498
+ for (const e of m.enums) {
499
+ lines.push(`fn ${toSnake(e.name)}_from_str(s: &str) -> backend::${e.name} {`);
500
+ lines.push(" match s {");
501
+ for (const v of e.values) {
502
+ lines.push(` "${v}" => backend::${e.name}::${v.toUpperCase()},`);
503
+ }
504
+ lines.push(` _ => backend::${e.name}::default(),`);
505
+ lines.push(" }");
506
+ lines.push("}");
507
+ lines.push("");
508
+ }
509
+
510
+ for (const t of m.tables) {
511
+ lines.push(`#[derive(Deserialize, Default)]`);
512
+ lines.push(`#[serde(rename_all = "camelCase")]`);
513
+ lines.push(`pub struct ${t.name}Json {`);
514
+ for (const f of t.fields) lines.push(emitJsonStruct(f));
515
+ lines.push("}");
516
+ lines.push("");
517
+ lines.push(emitBuildFn(m, t));
518
+ lines.push("");
519
+ }
520
+
521
+ for (const ev of m.events) {
522
+ lines.push(emitSerializeFn(m, ev));
523
+ lines.push("");
524
+ }
525
+
526
+ // Direct-args fast path (no JSON): one extern "C" fn per DIRECTABLE event type.
527
+ // Fields are passed as direct FFI args; vectors as packed blobs; out[0] = event id.
528
+ const readers = emitPackedReaders(m);
529
+ if (readers.length > 0) {
530
+ lines.push(...readers);
531
+ lines.push("");
532
+ }
533
+ for (const ev of m.events) {
534
+ if (!isDirectableEvent(m, ev)) continue;
535
+ lines.push(emitDirectFn(m, ev));
536
+ lines.push("");
537
+ }
538
+
539
+ lines.push(
540
+ "pub fn serialize_event(event_id: u32, json: &[u8], out: &mut [u8]) -> Result<usize, TranscodeError> {",
541
+ );
542
+ lines.push(" match event_id {");
543
+ m.events.forEach((ev) => {
544
+ lines.push(` ${eventId(ev.name)} => serialize_${toSnake(ev.name)}(json, out),`);
545
+ });
546
+ lines.push(" _ => Err(TranscodeError::UnknownEvent),");
547
+ lines.push(" }");
548
+ lines.push("}");
549
+ lines.push("");
550
+
551
+ return lines.join("\n");
552
+ }