@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.
- package/README.md +136 -33
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +292 -0
- package/docs/architecture.md +101 -28
- package/docs/events.md +252 -0
- package/docs/generic-bindings.md +207 -0
- package/docs/publishing.md +2 -2
- package/docs/wire-format.md +74 -20
- package/index.ts +75 -27
- package/package.json +13 -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 +510 -0
- package/public/internal.ts +16 -0
- package/public/nats.ts +9 -5
- package/public/server.ts +52 -16
- package/rust/src/ffi.rs +10 -0
- package/rust/src/generated/backend.rs +503 -0
- package/rust/src/transcode/generated.rs +377 -17
- 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/inbound.ts +46 -0
- package/src/bridge/nats/index.ts +131 -0
- package/src/bridge/nats/real-transport.ts +133 -0
- package/src/bridge/nats/types.ts +80 -0
- package/src/bridge/subjects.ts +3 -0
- package/src/codegen/constants.ts +28 -0
- package/src/codegen/direct-gen.ts +564 -0
- package/src/codegen/fingerprint.ts +44 -0
- package/src/codegen/hash.ts +25 -0
- package/src/codegen/registry-gen.ts +246 -0
- package/src/codegen/rust-glue-gen.ts +552 -0
- package/src/codegen/schema-model.ts +363 -0
- package/src/codegen/ts-ser-gen.ts +230 -0
- package/src/codegen/typebox-to-fbs.ts +60 -0
- package/src/core/auth.ts +67 -5
- package/src/core/client-heartbeat.ts +2 -1
- package/src/core/client-reconnect.ts +9 -2
- package/src/core/client-rpc.ts +75 -0
- package/src/core/client-state.ts +63 -8
- package/src/core/client-wire.ts +148 -15
- package/src/core/client.ts +105 -31
- package/src/core/groups.ts +8 -0
- package/src/core/metrics.ts +42 -21
- package/src/core/outbound.ts +62 -11
- package/src/core/rate-limit.ts +69 -0
- package/src/core/replay.ts +41 -1
- package/src/core/resume.ts +181 -0
- package/src/core/rooms.ts +10 -3
- package/src/core/routing.ts +144 -12
- package/src/core/server/client-info.ts +37 -0
- package/src/core/server/http-routes.ts +59 -0
- package/src/core/server/index.ts +360 -0
- package/src/core/server/metrics-view.ts +53 -0
- package/src/core/server/socket-lifecycle.ts +57 -0
- package/src/core/state.ts +124 -14
- package/src/core/topic-log.ts +86 -0
- package/src/events/clients.ts +174 -0
- package/src/events/cluster/dedupe.ts +43 -0
- package/src/events/cluster/envelope.ts +149 -0
- package/src/events/cluster/index.ts +50 -0
- package/src/events/cluster/keys.ts +33 -0
- package/src/events/cluster/kinds.ts +32 -0
- package/src/events/cluster/presence-table.ts +99 -0
- package/src/events/cluster/presence.ts +53 -0
- package/src/events/cluster/redis-client.ts +50 -0
- package/src/events/cluster/store-memory.ts +67 -0
- package/src/events/cluster/store-redis.ts +44 -0
- package/src/events/cluster/subjects.ts +30 -0
- package/src/events/cluster/sync.ts +476 -0
- package/src/events/cluster/transport-nats.ts +24 -0
- package/src/events/cluster/transport-redis.ts +120 -0
- package/src/events/cluster-rpc.ts +196 -0
- package/src/events/data.ts +38 -0
- package/src/events/delivery.ts +83 -0
- package/src/events/emit.ts +173 -0
- package/src/events/global.ts +117 -0
- package/src/events/groups.ts +118 -0
- package/src/events/hub/context-factory.ts +79 -0
- package/src/events/hub/dispatch.ts +86 -0
- package/src/events/hub/index.ts +536 -0
- package/src/events/hub/internal.ts +31 -0
- package/src/events/hub/metrics-snapshot.ts +84 -0
- package/src/events/hub/resolve-cluster.ts +49 -0
- package/src/events/index.ts +61 -0
- package/src/events/queue.ts +123 -0
- package/src/events/registry.ts +214 -0
- package/src/events/schedule.ts +73 -0
- package/src/events/trace.ts +283 -0
- package/src/events/types/client.ts +68 -0
- package/src/events/types/cluster.ts +40 -0
- package/src/events/types/context.ts +50 -0
- package/src/events/types/emit-target.ts +29 -0
- package/src/events/types/groups.ts +35 -0
- package/src/events/types/hub.ts +124 -0
- package/src/events/types/index.ts +30 -0
- package/src/events/types/metrics.ts +52 -0
- package/src/events/types/options.ts +62 -0
- package/src/generated/direct-ser.ts +148 -60
- package/src/generated/fbs/backend.fbs +24 -1
- package/src/generated/registry.ts +94 -33
- package/src/generated/rust/backend_generated.rs +503 -0
- package/src/generated/ts/backend.ts +4 -0
- package/src/generated/ts/resume.ts +74 -0
- package/src/generated/ts/resumed.ts +88 -0
- package/src/generated/ts/rpc-call.ts +112 -0
- package/src/generated/ts/rpc-result.ts +126 -0
- package/src/generated/ts/snapshot-request.ts +19 -5
- package/src/generated/ts-ser.ts +110 -17
- package/src/generated/wire-registry.json +7 -2
- package/src/native/ffi.ts +85 -28
- package/src/schema/index.ts +49 -2
- package/src/server.ts +7 -3
- package/src/transport/transport.ts +200 -79
- package/src/bridge/nats.ts +0 -269
- package/src/core/server.ts +0 -294
- 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
|
+
}
|