@ignex/nova 0.1.3 → 0.1.6

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 (98) hide show
  1. package/README.md +4 -1
  2. package/docs/ai/TREE.md +69 -9
  3. package/docs/architecture.md +75 -27
  4. package/docs/events.md +83 -1
  5. package/docs/generic-bindings.md +10 -0
  6. package/docs/wire-format.md +65 -18
  7. package/package.json +2 -1
  8. package/prebuilds/linux-x64/libignex_ffi.so +0 -0
  9. package/public/generate.ts +97 -3
  10. package/public/server.ts +10 -0
  11. package/rust/src/generated/backend.rs +503 -0
  12. package/rust/src/transcode/generated.rs +376 -17
  13. package/src/bridge/nats/inbound.ts +46 -0
  14. package/src/bridge/nats/index.ts +131 -0
  15. package/src/bridge/nats/real-transport.ts +133 -0
  16. package/src/bridge/nats/types.ts +80 -0
  17. package/src/codegen/constants.ts +14 -4
  18. package/src/codegen/direct-gen.ts +20 -6
  19. package/src/codegen/registry-gen.ts +10 -6
  20. package/src/codegen/rust-glue-gen.ts +10 -3
  21. package/src/codegen/schema-model.ts +28 -3
  22. package/src/codegen/ts-ser-gen.ts +12 -3
  23. package/src/core/auth.ts +65 -4
  24. package/src/core/client-rpc.ts +75 -0
  25. package/src/core/client-state.ts +53 -0
  26. package/src/core/client-wire.ts +183 -8
  27. package/src/core/client.ts +84 -4
  28. package/src/core/groups.ts +5 -0
  29. package/src/core/metrics.ts +38 -21
  30. package/src/core/outbound.ts +50 -6
  31. package/src/core/rate-limit.ts +69 -0
  32. package/src/core/replay.ts +41 -1
  33. package/src/core/resume.ts +181 -0
  34. package/src/core/rooms.ts +10 -3
  35. package/src/core/routing.ts +128 -5
  36. package/src/core/server/client-info.ts +37 -0
  37. package/src/core/server/http-routes.ts +59 -0
  38. package/src/core/{server.ts → server/index.ts} +112 -120
  39. package/src/core/server/metrics-view.ts +53 -0
  40. package/src/core/server/socket-lifecycle.ts +57 -0
  41. package/src/core/state.ts +73 -1
  42. package/src/core/topic-log.ts +86 -0
  43. package/src/events/clients.ts +18 -0
  44. package/src/events/cluster/dedupe.ts +43 -0
  45. package/src/events/cluster/envelope.ts +149 -0
  46. package/src/events/cluster/index.ts +50 -0
  47. package/src/events/cluster/keys.ts +33 -0
  48. package/src/events/cluster/kinds.ts +32 -0
  49. package/src/events/cluster/presence-table.ts +99 -0
  50. package/src/events/cluster/presence.ts +53 -0
  51. package/src/events/cluster/redis-client.ts +50 -0
  52. package/src/events/cluster/store-memory.ts +67 -0
  53. package/src/events/cluster/store-redis.ts +44 -0
  54. package/src/events/cluster/subjects.ts +30 -0
  55. package/src/events/cluster/sync.ts +476 -0
  56. package/src/events/cluster/transport-nats.ts +24 -0
  57. package/src/events/cluster/transport-redis.ts +120 -0
  58. package/src/events/cluster-rpc.ts +196 -0
  59. package/src/events/delivery.ts +83 -0
  60. package/src/events/emit.ts +57 -11
  61. package/src/events/hub/context-factory.ts +79 -0
  62. package/src/events/hub/dispatch.ts +86 -0
  63. package/src/events/hub/index.ts +536 -0
  64. package/src/events/hub/internal.ts +31 -0
  65. package/src/events/hub/metrics-snapshot.ts +84 -0
  66. package/src/events/hub/resolve-cluster.ts +49 -0
  67. package/src/events/queue.ts +36 -9
  68. package/src/events/registry.ts +90 -54
  69. package/src/events/schedule.ts +73 -0
  70. package/src/events/trace.ts +283 -0
  71. package/src/events/types/client.ts +68 -0
  72. package/src/events/types/cluster.ts +40 -0
  73. package/src/events/types/context.ts +50 -0
  74. package/src/events/types/emit-target.ts +29 -0
  75. package/src/events/types/groups.ts +35 -0
  76. package/src/events/types/hub.ts +124 -0
  77. package/src/events/types/index.ts +30 -0
  78. package/src/events/types/metrics.ts +52 -0
  79. package/src/events/types/options.ts +62 -0
  80. package/src/generated/direct-ser.ts +146 -59
  81. package/src/generated/fbs/backend.fbs +23 -0
  82. package/src/generated/registry.ts +92 -33
  83. package/src/generated/rust/backend_generated.rs +503 -0
  84. package/src/generated/ts/backend.ts +4 -0
  85. package/src/generated/ts/resume.ts +74 -0
  86. package/src/generated/ts/resumed.ts +88 -0
  87. package/src/generated/ts/rpc-call.ts +112 -0
  88. package/src/generated/ts/rpc-result.ts +126 -0
  89. package/src/generated/ts/snapshot-request.ts +19 -5
  90. package/src/generated/ts-ser.ts +109 -16
  91. package/src/generated/wire-registry.json +7 -3
  92. package/src/schema/index.ts +45 -1
  93. package/src/transport/transport.ts +117 -77
  94. package/src/bridge/nats.ts +0 -309
  95. package/src/events/cluster.ts +0 -732
  96. package/src/events/hub.ts +0 -481
  97. package/src/events/types.ts +0 -378
  98. package/src/transport/stats.ts +0 -48
package/src/events/hub.ts DELETED
@@ -1,481 +0,0 @@
1
- /**
2
- * Events hub — composition root for the events layer. Created by
3
- * `createServer({ events: {...} })` (exposed as `server.events`) and, by
4
- * default, bound to the module-global `emit` / `on` singleton.
5
- *
6
- * Wires the pieces together:
7
- * - `clients` — the active-connection registry (who is connected, on whose
8
- * behalf, per-connection data);
9
- * - `registry` — multi-handler dispatch (the events file receiving events);
10
- * - `emitter` — encode-once + local fan-out + bridge + offloaded cluster;
11
- * - `groups` — client groups (reusing the transport's group registry) and
12
- * user groups;
13
- * - `cluster` — optional cross-instance sync (NATS / Redis / custom) with
14
- * presence + shared-state indexes, all off the hot path via the queue.
15
- *
16
- * Lifecycle is driven by the server: `attach`/`detach` on socket open/close,
17
- * `onGroupChange` on any group mutation (auth seed, control frames,
18
- * programmatic), `dispatchBridgeInbound` for NATS-inbound events.
19
- */
20
- import type { ServerWebSocket } from "bun";
21
- import type { Bindings, DefaultBindings, EventNameOf } from "../bindings/types";
22
- import { createNatsBridge, type NatsBridge, type NatsBridgeOptions } from "../bridge/nats";
23
- import type { IgnServer } from "../core/server";
24
- import type { ServerState, WsData } from "../core/state";
25
- import { createClientStore } from "./clients";
26
- import {
27
- type ClusterSync,
28
- createClusterSync,
29
- createMemoryStateStore,
30
- createNatsClusterTransport,
31
- createRedisClusterTransport,
32
- } from "./cluster";
33
- import { createEmitter, deliverLocal, type EmitCounters } from "./emit";
34
- import { bindEvents, unbindEvents } from "./global";
35
- import { createGroupManager, type GroupManager } from "./groups";
36
- import { createTaskQueue } from "./queue";
37
- import { createHandlerRegistry } from "./registry";
38
- import type {
39
- ClientGroup,
40
- ClusterStateStore,
41
- ClusterTransport,
42
- EmitTarget,
43
- EventClient,
44
- EventContext,
45
- EventHandler,
46
- EventSource,
47
- EventsClusterOptions,
48
- EventsHub,
49
- EventsMetricsSnapshot,
50
- EventsOptions,
51
- UserGroup,
52
- } from "./types";
53
-
54
- /** Internal surface the server drives (not part of the public `EventsHub`). */
55
- export interface EventsHubInternal<B extends Bindings = DefaultBindings> extends EventsHub<B> {
56
- attach(ws: ServerWebSocket<WsData>): EventClient | undefined;
57
- detach(ws: ServerWebSocket<WsData>): EventClient | undefined;
58
- onGroupChange(group: string, ws: ServerWebSocket<WsData>, joined: boolean): void;
59
- dispatchBridgeInbound(name: string, payload: unknown): void;
60
- }
61
-
62
- export interface CreateEventsHubOptions<B extends Bindings> {
63
- state: ServerState;
64
- server: IgnServer<B>;
65
- bindings: Bindings;
66
- /** the server's own NATS bridge (reused by the cluster when `cluster.nats: true`) */
67
- serverBridge?: NatsBridge;
68
- options: EventsOptions<B>;
69
- }
70
-
71
- function resolveClusterTransport(
72
- opts: EventsClusterOptions,
73
- serverBridge: NatsBridge | undefined,
74
- bindings: Bindings,
75
- onError: (err: Error) => void,
76
- ): { transport?: ClusterTransport; ownBridge?: NatsBridge } {
77
- if (opts.transport) return { transport: opts.transport };
78
- if (opts.nats) {
79
- const nats = opts.nats;
80
- if (typeof nats === "boolean") {
81
- if (serverBridge) return { transport: createNatsClusterTransport(serverBridge) };
82
- const own = createNatsBridge(
83
- { servers: ["nats://localhost:4222"], inbound: false },
84
- undefined,
85
- bindings,
86
- );
87
- return { transport: createNatsClusterTransport(own), ownBridge: own };
88
- }
89
- if ("status" in nats) return { transport: createNatsClusterTransport(nats) };
90
- const own = createNatsBridge(nats as NatsBridgeOptions, undefined, bindings);
91
- return { transport: createNatsClusterTransport(own), ownBridge: own };
92
- }
93
- if (opts.redis) return { transport: createRedisClusterTransport(opts.redis, onError) };
94
- return {};
95
- }
96
-
97
- export function createEventsHub<B extends Bindings = DefaultBindings>(
98
- opts: CreateEventsHubOptions<B>,
99
- ): EventsHubInternal<B> {
100
- const { state, server, bindings, serverBridge } = opts;
101
- const eOpts = opts.options;
102
- const instanceId = eOpts.cluster?.instanceId ?? crypto.randomUUID();
103
- const prefix = eOpts.cluster?.prefix ?? bindings.subjectPrefix ?? "ignex";
104
- const queue = createTaskQueue({
105
- ...(eOpts.queue?.workers !== undefined ? { workers: eOpts.queue.workers } : {}),
106
- ...(eOpts.queue?.maxPending !== undefined ? { maxPending: eOpts.queue.maxPending } : {}),
107
- });
108
- const counters: EmitCounters = {
109
- emitted: 0,
110
- emittedByTarget: { broadcast: 0, topic: 0, group: 0, user: 0, client: 0 },
111
- deliveredLocal: 0,
112
- clusterPublished: 0,
113
- };
114
- let transportErrors = 0;
115
- let closed = false;
116
- let boundGlobal = false;
117
-
118
- const clients = createClientStore();
119
- const registry = createHandlerRegistry();
120
- const dispatchers = new Set<string>();
121
-
122
- // ── cluster ───────────────────────────────────────────────────────────
123
- const reportClusterError = (err: Error): void => {
124
- transportErrors++;
125
- void err;
126
- };
127
-
128
- let cluster: ClusterSync | undefined;
129
- let ownBridge: NatsBridge | undefined;
130
- let stateStore: ClusterStateStore | undefined;
131
- if (eOpts.cluster) {
132
- const resolved = resolveClusterTransport(
133
- eOpts.cluster,
134
- serverBridge,
135
- bindings,
136
- reportClusterError,
137
- );
138
- if (!resolved.transport) {
139
- throw new Error(
140
- "ignex events: cluster configured without a transport — set cluster.nats, cluster.redis, or cluster.transport",
141
- );
142
- }
143
- ownBridge = resolved.ownBridge;
144
- stateStore = eOpts.cluster.state ?? createMemoryStateStore();
145
- cluster = createClusterSync({
146
- instanceId,
147
- prefix,
148
- transport: resolved.transport,
149
- queue,
150
- bindings,
151
- stateStore,
152
- presenceTtlMs: eOpts.cluster.presenceTtlMs ?? 60_000,
153
- heartbeatMs: eOpts.cluster.heartbeatMs ?? 15_000,
154
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: cluster frame fan-out is inherently branchy
155
- onRemoteFrame: (kind, key, name, frame) => {
156
- let target: EmitTarget;
157
- if (kind === "broadcast") target = { type: "broadcast" };
158
- else if (kind === "topic") target = { type: "topic", topic: key };
159
- else if (kind === "group") target = { type: "group", group: key };
160
- else if (kind === "user") target = { type: "user", userId: key };
161
- else target = { type: "client", clientId: key };
162
- counters.deliveredLocal += deliverLocal(state, target, frame, userSockets);
163
- if (registry.wantsServerEvent(name)) {
164
- const id = bindings.anyEventNameToId[name];
165
- if (id !== undefined) {
166
- try {
167
- registry.dispatchServerEvent(
168
- name,
169
- bindings.decodePayload(id, frame),
170
- makeCtx(undefined, "remote"),
171
- );
172
- } catch (err) {
173
- reportClusterError(err instanceof Error ? err : new Error(String(err)));
174
- }
175
- }
176
- }
177
- },
178
- onError: reportClusterError,
179
- });
180
- }
181
-
182
- // ── emitter ───────────────────────────────────────────────────────────
183
- const userSockets = (userId: string): ServerWebSocket<WsData>[] =>
184
- clients.byUser(userId).map((c) => c.ws);
185
- const emitter = createEmitter({
186
- state,
187
- ...(serverBridge !== undefined ? { bridge: serverBridge } : {}),
188
- ...(cluster !== undefined ? { cluster } : {}),
189
- userSockets,
190
- counters,
191
- });
192
-
193
- // ── groups ────────────────────────────────────────────────────────────
194
- const groups: GroupManager<B> = createGroupManager<B>({
195
- state,
196
- emitToGroup: (group, name, payload) => api.emitToGroup(group, name as never, payload as never),
197
- emitToUser: (userId, name, payload) => api.emitToUser(userId, name as never, payload as never),
198
- onUserGroupChange: (name, members) => cluster?.userGroupChanged(name, members),
199
- });
200
-
201
- // ── context factory ───────────────────────────────────────────────────
202
- const makeCtx = (client: EventClient | undefined, source: EventSource): EventContext<B> => {
203
- const ctx: EventContext<B> = {
204
- source,
205
- hub: api,
206
- server,
207
- emit: (name, payload, target) => api.emit(name as never, payload as never, target),
208
- emitToGroup: (group, name, payload) =>
209
- api.emitToGroup(group, name as never, payload as never),
210
- emitToUser: (userId, name, payload) =>
211
- api.emitToUser(userId, name as never, payload as never),
212
- emitToClient: (clientId, name, payload) =>
213
- api.emitToClient(clientId, name as never, payload as never),
214
- emitToTopic: (topic, name, payload) =>
215
- api.emitToTopic(topic, name as never, payload as never),
216
- ...(client ? { client } : {}),
217
- };
218
- return ctx;
219
- };
220
-
221
- // ── inbound dispatch (client-sent events → handlers) ──────────────────
222
- const dispatchClientEvent = (
223
- name: string,
224
- payload: unknown,
225
- ws: ServerWebSocket<WsData>,
226
- ): void => {
227
- const client = clients.get(ws.data.id) ?? attach(ws);
228
- registry.dispatch(name, payload, makeCtx(client, "client"));
229
- };
230
-
231
- const ensureDispatcher = (name: string): void => {
232
- if (dispatchers.has(name)) return;
233
- dispatchers.add(name);
234
- server.allowInbound(name as never);
235
- server.on(
236
- name as never,
237
- ((payload: unknown, ws: ServerWebSocket<WsData>) =>
238
- dispatchClientEvent(name, payload, ws)) as never,
239
- );
240
- };
241
-
242
- // ── client lifecycle ──────────────────────────────────────────────────
243
- const attach = (ws: ServerWebSocket<WsData>): EventClient | undefined => {
244
- const client = clients.attach(ws);
245
- if (!client) return undefined;
246
- cluster?.clientJoined({
247
- id: client.id,
248
- ...(client.userId !== undefined ? { userId: client.userId } : {}),
249
- });
250
- eOpts.onConnect?.(client);
251
- return client;
252
- };
253
-
254
- const detach = (ws: ServerWebSocket<WsData>): EventClient | undefined => {
255
- const client = clients.detach(ws);
256
- if (!client) return undefined;
257
- cluster?.clientLeft({
258
- id: client.id,
259
- ...(client.userId !== undefined ? { userId: client.userId } : {}),
260
- });
261
- eOpts.onDisconnect?.(client);
262
- return client;
263
- };
264
-
265
- const onGroupChange = (group: string, ws: ServerWebSocket<WsData>, joined: boolean): void => {
266
- cluster?.clientGroupChanged(group, ws.data.id, joined);
267
- };
268
-
269
- // ── the hub API ───────────────────────────────────────────────────────
270
- const api: EventsHubInternal<B> = {
271
- get server(): IgnServer<B> {
272
- return server;
273
- },
274
- get instanceId(): string {
275
- return instanceId;
276
- },
277
-
278
- on(name, handler) {
279
- ensureDispatcher(name);
280
- registry.on(name, handler as never);
281
- return api;
282
- },
283
- off(name, handler) {
284
- registry.off(name, handler as never);
285
- return api;
286
- },
287
- once(name, handler) {
288
- ensureDispatcher(name);
289
- registry.once(name, handler as never);
290
- return api;
291
- },
292
- onAny(cb) {
293
- registry.onAny(cb as never);
294
- return api;
295
- },
296
- offAny(cb) {
297
- registry.offAny(cb as never);
298
- return api;
299
- },
300
- onServerEvent(name, handler) {
301
- registry.onServerEvent(name, handler as never);
302
- return api;
303
- },
304
- offServerEvent(name, handler) {
305
- registry.offServerEvent(name, handler as never);
306
- return api;
307
- },
308
- events() {
309
- return registry.names() as EventNameOf<B>[];
310
- },
311
- listenerCount(name) {
312
- return registry.count(name);
313
- },
314
- removeAllListeners(name) {
315
- registry.removeAll(name);
316
- return api;
317
- },
318
-
319
- emit(name, payload, target = { type: "broadcast" }) {
320
- if (closed) return;
321
- emitter.emit(name as never, payload as never, target);
322
- },
323
- emitToTopic(topic, name, payload) {
324
- api.emit(name, payload, { type: "topic", topic });
325
- },
326
- emitToGroup(group, name, payload) {
327
- api.emit(name, payload, { type: "group", group });
328
- },
329
- emitToUser(userId, name, payload) {
330
- api.emit(name, payload, { type: "user", userId });
331
- },
332
- emitToClient(clientId, name, payload) {
333
- api.emit(name, payload, { type: "client", clientId });
334
- },
335
-
336
- client(id) {
337
- return clients.get(id);
338
- },
339
- clients() {
340
- return clients.all();
341
- },
342
- clientsByUser(userId) {
343
- return clients.byUser(userId);
344
- },
345
- get clientCount(): number {
346
- return clients.size;
347
- },
348
- setUserId(clientId, userId) {
349
- const before = clients.get(clientId)?.userId;
350
- if (!clients.setUserId(clientId, userId)) return;
351
- if (before !== userId) {
352
- cluster?.clientLeft({
353
- id: clientId,
354
- ...(before !== undefined ? { userId: before } : {}),
355
- });
356
- cluster?.clientJoined({
357
- id: clientId,
358
- ...(userId !== undefined ? { userId } : {}),
359
- });
360
- }
361
- },
362
- setClientData(clientId, key, value) {
363
- const client = clients.get(clientId);
364
- if (!client) return;
365
- client.data.set(key, value);
366
- cluster?.setRemoteClientData(clientId, JSON.stringify(client.data.toJSON()));
367
- },
368
- getClientData(clientId, key) {
369
- return clients.get(clientId)?.data.get(key);
370
- },
371
- clearClientData(clientId) {
372
- const client = clients.get(clientId);
373
- if (!client) return;
374
- client.data.clear();
375
- cluster?.setRemoteClientData(clientId, "{}");
376
- },
377
-
378
- group(name) {
379
- return groups.clientGroup(name);
380
- },
381
- groups() {
382
- return groups.clientGroups();
383
- },
384
- userGroup(name) {
385
- return groups.userGroup(name);
386
- },
387
- userGroups() {
388
- return groups.userGroups();
389
- },
390
-
391
- clusterClients() {
392
- return cluster?.remoteClients() ?? [];
393
- },
394
- async clusterUserClients(userId) {
395
- return (await cluster?.clusterUserClients(userId)) ?? [];
396
- },
397
- async clusterGroupMembers(group) {
398
- return (await cluster?.clusterGroupMembers(group)) ?? [];
399
- },
400
- async clusterUserGroupMembers(group) {
401
- return (await cluster?.clusterUserGroupMembers(group)) ?? [];
402
- },
403
- async remoteClientData(clientId) {
404
- return await cluster?.getRemoteClientData(clientId);
405
- },
406
-
407
- metrics() {
408
- const s = cluster?.stats();
409
- const q = queue.stats();
410
- const out: EventsMetricsSnapshot = {
411
- emitted: counters.emitted,
412
- emittedByTarget: { ...counters.emittedByTarget },
413
- deliveredLocal: counters.deliveredLocal,
414
- clusterPublished: counters.clusterPublished,
415
- clusterReceived: s?.received ?? 0,
416
- clusterDroppedSelf: s?.droppedSelf ?? 0,
417
- clusterErrors: (s?.errors ?? 0) + transportErrors,
418
- queueQueued: q.queued,
419
- queueDropped: q.dropped,
420
- queueErrors: q.errors,
421
- handlerErrors: registry.errorCount,
422
- connectedClients: clients.size,
423
- remoteClients: cluster?.remoteClients().length ?? 0,
424
- clientGroups: groups.clientGroups().length,
425
- userGroups: groups.userGroups().length,
426
- };
427
- return out;
428
- },
429
- queueStats() {
430
- const s = queue.stats();
431
- return { pending: queue.pending, ...s };
432
- },
433
-
434
- // ── internals (server-driven) ───────────────────────────────────────
435
- attach,
436
- detach,
437
- onGroupChange,
438
- dispatchBridgeInbound(name, payload) {
439
- if (registry.wantsServerEvent(name)) {
440
- registry.dispatchServerEvent(name, payload, makeCtx(undefined, "bridge"));
441
- }
442
- },
443
-
444
- async close() {
445
- if (closed) return;
446
- closed = true;
447
- if (boundGlobal) unbindEvents();
448
- // announce local leaves so remote presence converges before we shut down
449
- for (const c of clients.all())
450
- cluster?.clientLeft({
451
- id: c.id,
452
- ...(c.userId !== undefined ? { userId: c.userId } : {}),
453
- });
454
- await cluster?.close();
455
- await queue.drain();
456
- queue.close();
457
- if (ownBridge) await ownBridge.close();
458
- },
459
- };
460
-
461
- if (eOpts.global !== false) {
462
- bindEvents(api as never);
463
- boundGlobal = true;
464
- }
465
-
466
- // pre-register dispatchers for explicitly allowed inbound events (so onAny
467
- // sees them even without a dedicated handler)
468
- for (const name of eOpts.inbound ?? []) ensureDispatcher(name);
469
-
470
- return api;
471
- }
472
-
473
- export type {
474
- ClientGroup,
475
- ClusterStateStore,
476
- EmitTarget,
477
- EventHandler,
478
- EventsHub,
479
- EventsOptions,
480
- UserGroup,
481
- };