@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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups — named fan-out dimensions with explicit differentiation:
|
|
3
|
+
*
|
|
4
|
+
* - CLIENT groups (`hub.group(name)`): membership by connection id. These
|
|
5
|
+
* reuse the transport's group registry (`state.groups`), so auth-seeded
|
|
6
|
+
* groups, control frames (`client.joinGroup`), `server.joinGroup` and the
|
|
7
|
+
* events layer all see the SAME membership.
|
|
8
|
+
* - USER groups (`hub.userGroup(name)`): membership by `userId`. Emitting to
|
|
9
|
+
* a user group fans out to every socket acting on behalf of each member
|
|
10
|
+
* user — the "broadcast to a group of people" case, independent of how
|
|
11
|
+
* many devices each person has open.
|
|
12
|
+
*
|
|
13
|
+
* Both are cluster-aware at the hub level: emit → `emitToGroup` / `emitToUser`
|
|
14
|
+
* (which publish to the cluster subjects), and membership syncs to the shared
|
|
15
|
+
* state store when one is configured.
|
|
16
|
+
*/
|
|
17
|
+
import type { ServerWebSocket } from "bun";
|
|
18
|
+
import type { Bindings, DefaultBindings, EventNameOf, EventsOf } from "../bindings/types";
|
|
19
|
+
import { activeGroups, groupMembers as stateGroupMembers, joinGroup, leaveGroup } from "../core/groups";
|
|
20
|
+
import type { ServerState, WsData } from "../core/state";
|
|
21
|
+
import type { ClientGroup, UserGroup } from "./types";
|
|
22
|
+
|
|
23
|
+
export interface GroupManager<B extends Bindings = DefaultBindings> {
|
|
24
|
+
clientGroup(name: string): ClientGroup<B>;
|
|
25
|
+
clientGroups(): string[];
|
|
26
|
+
userGroup(name: string): UserGroup<B>;
|
|
27
|
+
userGroups(): string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GroupManagerOptions<B extends Bindings = DefaultBindings> {
|
|
31
|
+
state: ServerState;
|
|
32
|
+
emitToGroup: <K extends EventNameOf<B>>(group: string, name: K, payload: EventsOf<B>[K]) => void;
|
|
33
|
+
emitToUser: <K extends EventNameOf<B>>(userId: string, name: K, payload: EventsOf<B>[K]) => void;
|
|
34
|
+
/** fired on user-group membership change (hub wires cluster sync here) */
|
|
35
|
+
onUserGroupChange?: (name: string, members: ReadonlySet<string>) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createGroupManager<B extends Bindings = DefaultBindings>(opts: GroupManagerOptions<B>): GroupManager<B> {
|
|
39
|
+
const userGroupsMap = new Map<string, Set<string>>();
|
|
40
|
+
|
|
41
|
+
const touchUserGroup = (name: string): void => {
|
|
42
|
+
opts.onUserGroupChange?.(name, userGroupsMap.get(name) ?? new Set());
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const userGroupHandle = (name: string): UserGroup<B> => {
|
|
46
|
+
let set = userGroupsMap.get(name);
|
|
47
|
+
if (!set) {
|
|
48
|
+
set = new Set();
|
|
49
|
+
userGroupsMap.set(name, set);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
get name(): string {
|
|
53
|
+
return name;
|
|
54
|
+
},
|
|
55
|
+
add(userId) {
|
|
56
|
+
set!.add(userId);
|
|
57
|
+
touchUserGroup(name);
|
|
58
|
+
},
|
|
59
|
+
remove(userId) {
|
|
60
|
+
const existed = set!.delete(userId);
|
|
61
|
+
if (existed) {
|
|
62
|
+
if (set!.size === 0) userGroupsMap.delete(name);
|
|
63
|
+
touchUserGroup(name);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
has(userId) {
|
|
67
|
+
return set!.has(userId);
|
|
68
|
+
},
|
|
69
|
+
members() {
|
|
70
|
+
return [...set!];
|
|
71
|
+
},
|
|
72
|
+
get size(): number {
|
|
73
|
+
return set!.size;
|
|
74
|
+
},
|
|
75
|
+
emit(event, payload) {
|
|
76
|
+
const snapshot = [...set!];
|
|
77
|
+
for (const userId of snapshot) opts.emitToUser(userId, event, payload);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const clientGroupHandle = (name: string): ClientGroup<B> => {
|
|
83
|
+
const socket = (clientId: string): ServerWebSocket<WsData> | undefined => opts.state.clients.get(clientId);
|
|
84
|
+
return {
|
|
85
|
+
get name(): string {
|
|
86
|
+
return name;
|
|
87
|
+
},
|
|
88
|
+
add(clientId) {
|
|
89
|
+
const ws = socket(clientId);
|
|
90
|
+
if (ws) joinGroup(opts.state, ws, name);
|
|
91
|
+
},
|
|
92
|
+
remove(clientId) {
|
|
93
|
+
const ws = socket(clientId);
|
|
94
|
+
if (ws) leaveGroup(opts.state, ws, name);
|
|
95
|
+
},
|
|
96
|
+
has(clientId) {
|
|
97
|
+
const ws = socket(clientId);
|
|
98
|
+
return ws !== undefined && ws.data.groups.has(name);
|
|
99
|
+
},
|
|
100
|
+
members() {
|
|
101
|
+
return stateGroupMembers(opts.state, name);
|
|
102
|
+
},
|
|
103
|
+
get size(): number {
|
|
104
|
+
return stateGroupMembers(opts.state, name).length;
|
|
105
|
+
},
|
|
106
|
+
emit(event, payload) {
|
|
107
|
+
opts.emitToGroup(name, event, payload);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
clientGroup: clientGroupHandle,
|
|
114
|
+
clientGroups: () => activeGroups(opts.state),
|
|
115
|
+
userGroup: userGroupHandle,
|
|
116
|
+
userGroups: () => [...userGroupsMap.keys()],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context factory — builds and CACHES the {@link EventContext} handed to
|
|
3
|
+
* handlers.
|
|
4
|
+
*
|
|
5
|
+
* Contexts are CACHED (instantiation-time work, not per-event work): every
|
|
6
|
+
* EventContext is immutable and its closures are bound to the stable hub API,
|
|
7
|
+
* so one context per client record + one shared context per client-less
|
|
8
|
+
* source is built once and reused — dispatching an event allocates nothing.
|
|
9
|
+
*
|
|
10
|
+
* The hub reference is injected lazily (`getHub`) because the factory is
|
|
11
|
+
* constructed before the hub API object exists.
|
|
12
|
+
*/
|
|
13
|
+
import type { ServerWebSocket } from "bun";
|
|
14
|
+
import type { Bindings } from "../../bindings/types";
|
|
15
|
+
import type { WsData } from "../../core/state";
|
|
16
|
+
import type { EventClient, EventContext, EventSource } from "../types";
|
|
17
|
+
|
|
18
|
+
export interface ContextFactory<B extends Bindings> {
|
|
19
|
+
/** context for a client-sent event (cached per client record) */
|
|
20
|
+
forClient(client: EventClient): EventContext<B>;
|
|
21
|
+
/** full form: explicit client + source (client-less sources share one ctx) */
|
|
22
|
+
make(client: EventClient | undefined, source: EventSource): EventContext<B>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createContextFactory<B extends Bindings>(deps: {
|
|
26
|
+
server: IgnServerOf<B>;
|
|
27
|
+
getHub: () => EventsHubOf<B>;
|
|
28
|
+
}): ContextFactory<B> {
|
|
29
|
+
const { server, getHub } = deps;
|
|
30
|
+
|
|
31
|
+
/** Assemble a fresh context bound to the stable hub API. */
|
|
32
|
+
const buildCtx = (client: EventClient | undefined, source: EventSource): EventContext<B> => {
|
|
33
|
+
const hub = getHub();
|
|
34
|
+
const ctx: EventContext<B> = {
|
|
35
|
+
source,
|
|
36
|
+
hub,
|
|
37
|
+
server,
|
|
38
|
+
emit: (name, payload, target) => hub.emit(name as never, payload as never, target),
|
|
39
|
+
emitToGroup: (group, name, payload) => hub.emitToGroup(group, name as never, payload as never),
|
|
40
|
+
emitToUser: (userId, name, payload) => hub.emitToUser(userId, name as never, payload as never),
|
|
41
|
+
emitToClient: (clientId, name, payload) =>
|
|
42
|
+
hub.emitToClient(clientId, name as never, payload as never),
|
|
43
|
+
emitToTopic: (topic, name, payload) => hub.emitToTopic(topic, name as never, payload as never),
|
|
44
|
+
...(client ? { client } : {}),
|
|
45
|
+
};
|
|
46
|
+
return ctx;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// caches: per-client contexts + one shared context per client-less source
|
|
50
|
+
const ctxByClient = new WeakMap<object, EventContext<B>>();
|
|
51
|
+
const sharedCtxs = new Map<EventSource, EventContext<B>>();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
forClient(client) {
|
|
55
|
+
let ctx = ctxByClient.get(client);
|
|
56
|
+
if (ctx === undefined) {
|
|
57
|
+
ctx = buildCtx(client, "client");
|
|
58
|
+
ctxByClient.set(client, ctx);
|
|
59
|
+
}
|
|
60
|
+
return ctx;
|
|
61
|
+
},
|
|
62
|
+
make(client, source) {
|
|
63
|
+
if (client === undefined) {
|
|
64
|
+
let shared = sharedCtxs.get(source);
|
|
65
|
+
if (shared === undefined) {
|
|
66
|
+
shared = buildCtx(undefined, source);
|
|
67
|
+
sharedCtxs.set(source, shared);
|
|
68
|
+
}
|
|
69
|
+
return shared;
|
|
70
|
+
}
|
|
71
|
+
return this.forClient(client);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Structural stand-ins to keep this module free of circular type imports. */
|
|
77
|
+
type IgnServerOf<B extends Bindings> = import("../../core/server").IgnServer<B>;
|
|
78
|
+
type EventsHubOf<B extends Bindings> = import("../types").EventsHub<B>;
|
|
79
|
+
export type HubSocket = ServerWebSocket<WsData>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler dispatch — routes an inbound event to the registry, optionally with
|
|
3
|
+
* the reliability layer (retry + DLQ). When no policy is configured this is a
|
|
4
|
+
* plain fire-and-forget dispatch: zero overhead on the hot path.
|
|
5
|
+
*/
|
|
6
|
+
import type { EventContext } from "../types";
|
|
7
|
+
import type { HandlerRegistry } from "../registry";
|
|
8
|
+
import {
|
|
9
|
+
deliverWithRetry,
|
|
10
|
+
resolveDeliveryPolicy,
|
|
11
|
+
type DeliveryPolicy,
|
|
12
|
+
type ResolvedDeliveryPolicy,
|
|
13
|
+
} from "../delivery";
|
|
14
|
+
|
|
15
|
+
/** How the hub counts reliability activity (folded into metrics). */
|
|
16
|
+
export interface DispatchCounters {
|
|
17
|
+
/** retry schedules by the delivery layer */
|
|
18
|
+
handlerRetries: number;
|
|
19
|
+
/** events dead-lettered after exhausting retries */
|
|
20
|
+
dlqCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the resolved policy + registry pair used by {@link createDispatcher}.
|
|
25
|
+
* Returns `policy: null` when handlers are unconfigured (plain dispatch).
|
|
26
|
+
*/
|
|
27
|
+
export function resolveDispatchPolicy(
|
|
28
|
+
opts: DeliveryPolicy | undefined,
|
|
29
|
+
counters: DispatchCounters,
|
|
30
|
+
): ResolvedDeliveryPolicy | null {
|
|
31
|
+
if (!opts) return null;
|
|
32
|
+
return resolveDeliveryPolicy({
|
|
33
|
+
...opts,
|
|
34
|
+
dlq: (info) => {
|
|
35
|
+
counters.dlqCount++;
|
|
36
|
+
opts.dlq?.(info);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Dispatcher<B extends import("../../bindings/types").Bindings> {
|
|
42
|
+
/** dispatch a client-sent event */
|
|
43
|
+
client(name: string, payload: unknown, ctx: EventContext<B>): void;
|
|
44
|
+
/** dispatch a server-side event (remote instance / bridge origin) */
|
|
45
|
+
server(name: string, payload: unknown, ctx: EventContext<B>): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reliability-aware dispatcher. `mode` selects the handler surface
|
|
50
|
+
* (client-sent vs server-side); retries are counted into `counters`.
|
|
51
|
+
*/
|
|
52
|
+
export function createDispatcher<B extends import("../../bindings/types").Bindings>(deps: {
|
|
53
|
+
registry: HandlerRegistry & { settleDispatch: HandlerRegistry["settleDispatch"] };
|
|
54
|
+
policy: ResolvedDeliveryPolicy | null;
|
|
55
|
+
counters: DispatchCounters;
|
|
56
|
+
}): Dispatcher<B> {
|
|
57
|
+
const { registry, policy, counters } = deps;
|
|
58
|
+
const dispatch = (
|
|
59
|
+
name: string,
|
|
60
|
+
payload: unknown,
|
|
61
|
+
ctx: EventContext<B>,
|
|
62
|
+
mode: "client" | "server",
|
|
63
|
+
): void => {
|
|
64
|
+
if (policy === null) {
|
|
65
|
+
// default: plain dispatch, zero overhead
|
|
66
|
+
if (mode === "server") registry.dispatchServerEvent(name, payload, ctx);
|
|
67
|
+
else registry.dispatch(name, payload, ctx);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void deliverWithRetry(
|
|
71
|
+
registry,
|
|
72
|
+
policy,
|
|
73
|
+
name,
|
|
74
|
+
payload,
|
|
75
|
+
ctx,
|
|
76
|
+
() => {
|
|
77
|
+
counters.handlerRetries++;
|
|
78
|
+
},
|
|
79
|
+
mode,
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
client: (name, payload, ctx) => dispatch(name, payload, ctx, "client"),
|
|
84
|
+
server: (name, payload, ctx) => dispatch(name, payload, ctx, "server"),
|
|
85
|
+
};
|
|
86
|
+
}
|