@super-line/server 0.1.0 → 0.3.0

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/dist/index.d.cts CHANGED
@@ -1,7 +1,14 @@
1
1
  import { Server, IncomingMessage } from 'node:http';
2
- import { ServerMessageDef, Serializer, ServerFrame, EmitData, Adapter, Contract, RoleOf, SharedRequests, ServerInput, SharedEvents, Output, RoleRequests, Events, RoleTopics, SharedTopics, ServerEvents, ServerEmit, ServerData } from '@super-line/core';
2
+ import { ServerMessageDef, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData } from '@super-line/core';
3
3
  import { WebSocket } from 'ws';
4
4
 
5
+ /** Backpressure policy: what to do when a connection's send buffer grows too large. */
6
+ interface Backpressure {
7
+ /** Buffer size (bytes) above which {@link Backpressure.onExceed} kicks in. */
8
+ maxBufferedBytes: number;
9
+ /** `'close'` (default) drops the connection with code 1013; `'drop'` skips the frame. */
10
+ onExceed?: 'close' | 'drop';
11
+ }
5
12
  /**
6
13
  * A single client connection, passed to handlers as the third argument.
7
14
  *
@@ -10,23 +17,43 @@ import { WebSocket } from 'ws';
10
17
  * (use a per-user room instead). Generic over the events it may emit (scoped by
11
18
  * role), its `ctx`, and its `role`.
12
19
  */
13
- declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string> {
20
+ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string, Data = unknown> {
14
21
  /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
15
22
  readonly ws: WebSocket;
23
+ /** Server-assigned unique id for this connection (stable for its lifetime). */
24
+ readonly id: string;
16
25
  /** This connection's role (the literal resolved by `authenticate`). */
17
26
  readonly role: Role;
18
27
  /** The context `authenticate` returned for this connection. */
19
28
  readonly ctx: Ctx;
20
29
  private readonly serializer;
30
+ private readonly backpressure?;
31
+ /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
32
+ private readonly onEmit?;
21
33
  /** Namespaced channels (rooms + topics) this connection belongs to. */
22
34
  readonly channels: Set<string>;
35
+ /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
+ data: Data;
37
+ /** When this connection was accepted (`Date.now()` at the upgrade). */
38
+ readonly connectedAt: number;
39
+ /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
+ lastPingAt?: number;
41
+ /** When a heartbeat pong was last received — liveness signal (managed by the server). */
42
+ lastPongAt?: number;
43
+ /** Pings sent since the last pong; drives reaping (managed by the server). */
44
+ missedPongs: number;
23
45
  constructor(
24
46
  /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
25
47
  ws: WebSocket,
48
+ /** Server-assigned unique id for this connection (stable for its lifetime). */
49
+ id: string,
26
50
  /** This connection's role (the literal resolved by `authenticate`). */
27
51
  role: Role,
28
52
  /** The context `authenticate` returned for this connection. */
29
- ctx: Ctx, serializer: Serializer);
53
+ ctx: Ctx, serializer: Serializer, backpressure?: Backpressure | undefined,
54
+ /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
55
+ onEmit?: ((event: string, data: unknown) => void) | undefined);
56
+ private overBackpressure;
30
57
  /** Encode and send a frame (unicast, e.g. req/res). */
31
58
  send(frame: ServerFrame): void;
32
59
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
@@ -40,9 +67,12 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
40
67
  /**
41
68
  * In-process pub/sub bus. Share one bus across multiple servers to simulate
42
69
  * multiple nodes in a test (each server gets its own adapter bound to the bus).
70
+ * The presence directory also lives here, so servers sharing a bus see the whole
71
+ * cluster (mirroring how Redis is shared in production).
43
72
  */
44
73
  declare class MemoryBus {
45
74
  private readonly channels;
75
+ readonly descriptors: Map<string, ConnDescriptor>;
46
76
  subscribe(channel: string, adapter: MemoryAdapter): void;
47
77
  unsubscribe(channel: string, adapter: MemoryAdapter): void;
48
78
  publish(channel: string, payload: string | Uint8Array): void;
@@ -50,6 +80,7 @@ declare class MemoryBus {
50
80
  declare class MemoryAdapter implements Adapter {
51
81
  private readonly bus;
52
82
  private handler?;
83
+ readonly presence: PresenceStore;
53
84
  constructor(bus: MemoryBus);
54
85
  subscribe(channel: string): void;
55
86
  unsubscribe(channel: string): void;
@@ -83,10 +114,10 @@ type CtxUnion<A> = A extends {
83
114
  ctx: infer X;
84
115
  } ? X : never;
85
116
  type RoleHandlers<C extends Contract, A, R extends RoleOf<C>> = {
86
- [K in keyof RoleRequests<C, R>]: (input: ServerInput<RoleRequests<C, R>[K]>, ctx: CtxFor<A, R>, conn: Conn<Events<C, R>, CtxFor<A, R>, R>) => Awaitable<Output<RoleRequests<C, R>[K]>>;
117
+ [K in keyof RoleRequests<C, R>]: (input: ServerInput<RoleRequests<C, R>[K]>, ctx: CtxFor<A, R>, conn: Conn<Events<C, R>, CtxFor<A, R>, R, DataOf<C, R>>) => Awaitable<Output<RoleRequests<C, R>[K]>>;
87
118
  };
88
119
  type SharedHandlers<C extends Contract, A> = {
89
- [K in keyof SharedRequests<C>]: (input: ServerInput<SharedRequests<C>[K]>, ctx: CtxUnion<A>, conn: Conn<SharedEvents<C>, CtxUnion<A>, RoleOf<C>>) => Awaitable<Output<SharedRequests<C>[K]>>;
120
+ [K in keyof SharedRequests<C>]: (input: ServerInput<SharedRequests<C>[K]>, ctx: CtxUnion<A>, conn: Conn<SharedEvents<C>, CtxUnion<A>, RoleOf<C>, AnyData<C>>) => Awaitable<Output<SharedRequests<C>[K]>>;
90
121
  };
91
122
  /**
92
123
  * The handler map passed to `implement`: one block per role plus an optional
@@ -100,12 +131,17 @@ type Handlers<C extends Contract, A> = ([keyof SharedRequests<C>] extends [never
100
131
  };
101
132
  /** Context passed to middleware and lifecycle hooks about the current operation. */
102
133
  interface MiddlewareInfo {
103
- /** Whether this is a request or a topic subscribe. */
104
- kind: 'request' | 'subscribe';
105
- /** The request/topic name. */
134
+ /** Whether this is a request, a topic subscribe, or a bus event delivery. */
135
+ kind: 'request' | 'subscribe' | 'event';
136
+ /** The request/topic/event name. */
106
137
  name: string;
107
- /** The connection the operation is on (`conn.role` available). */
108
- conn: Conn;
138
+ /** The connection the operation is on, if any (`conn.role` available). Absent for bus events. */
139
+ conn?: Conn;
140
+ }
141
+ /** Metadata passed to a {@link SuperLineServer.subscribe} callback alongside the event payload. */
142
+ interface BusMeta {
143
+ /** The node that published the event. Equals `srv.nodeId` for a same-node publish (local echo). */
144
+ from: string;
109
145
  }
110
146
  /**
111
147
  * Flat middleware run before request/subscribe handlers. Call `next()` to proceed,
@@ -125,14 +161,64 @@ interface Room<C extends Contract> {
125
161
  broadcast<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
126
162
  /** Member count **on the current node** (membership is node-local). */
127
163
  readonly size: number;
164
+ /** Snapshot of this room's members **on the current node**. */
165
+ readonly connections: Conn[];
166
+ }
167
+ /** Synchronous, node-local introspection of the current server process. */
168
+ interface LocalView {
169
+ /** Snapshot of all connections accepted on this node. */
170
+ readonly connections: Conn[];
171
+ /** Names of rooms with at least one member on this node. */
172
+ readonly rooms: string[];
173
+ /** Names of topics with at least one subscriber on this node. */
174
+ readonly topics: string[];
175
+ }
176
+ /**
177
+ * Asynchronous, cluster-wide introspection backed by the adapter's presence
178
+ * directory. Methods reject if the configured adapter has no presence support.
179
+ */
180
+ interface ClusterView {
181
+ /** Every live connection across the cluster. */
182
+ connections(): Promise<ConnDescriptor[]>;
183
+ /** Total live connection count across the cluster. */
184
+ count(): Promise<number>;
185
+ /** Connections for a given user key (the `identify` hook). */
186
+ byUser(userId: string): Promise<ConnDescriptor[]>;
187
+ /** Connections that are members of `room`, across nodes. */
188
+ room(name: string): Promise<ConnDescriptor[]>;
189
+ /** Per-node aggregates (the other nodes and their counts). */
190
+ topology(): Promise<NodeStat[]>;
191
+ }
192
+ /** A single targeted connection, reachable on whatever node holds it. */
193
+ interface ConnTarget<C extends Contract> {
194
+ /** Push a shared event to this connection (cross-node). */
195
+ emit<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
196
+ /**
197
+ * Send a shared server→client request and await the client's typed reply
198
+ * (cross-node). Rejects with a `TIMEOUT` `SuperLineError` if no live node owns
199
+ * the connection or the client doesn't answer in time.
200
+ */
201
+ request<M extends keyof SharedServerRequests<C>>(name: M, input: ClientInput<SharedServerRequests<C>[M]>, opts?: {
202
+ timeout?: number;
203
+ signal?: AbortSignal;
204
+ }): Promise<Output<SharedServerRequests<C>[M]>>;
205
+ /** Close this connection (cross-node kick). */
206
+ close(): void;
207
+ }
208
+ /** All of a user's connections (0..N devices), reachable across nodes. */
209
+ interface UserTarget<C extends Contract> {
210
+ /** Push a shared event to every one of the user's connections (cross-node). */
211
+ emit<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
212
+ /** Disconnect every one of the user's connections (cross-node). */
213
+ disconnect(): void;
128
214
  }
129
215
  /** Lens for role-scoped server sends, returned by `srv.forRole(role)`. */
130
216
  interface RoleLens<C extends Contract, R extends RoleOf<C>> {
131
217
  /** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
132
218
  publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
133
219
  }
134
- /** Options for {@link createSocketServer}. */
135
- interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
220
+ /** Options for {@link createSuperLineServer}. */
221
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
136
222
  /** The `http.Server` to attach to (compose with Express/Fastify/Hono). */
137
223
  server?: Server;
138
224
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -141,12 +227,41 @@ interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
141
227
  adapter?: Adapter;
142
228
  /** Only handle upgrades for this pathname; others are left untouched. */
143
229
  path?: string;
230
+ /**
231
+ * Friendly name for this node, surfaced in `srv.nodeName`, the cluster descriptor, and the
232
+ * Control Center topology. Defaults to `SUPER_LINE_NODE_NAME` or a short slice of `nodeId`.
233
+ */
234
+ nodeName?: string;
144
235
  /** Runs at the HTTP upgrade. Return { role, ctx }, or throw to reject with 401. */
145
236
  authenticate: (req: IncomingMessage) => Awaitable<A>;
237
+ /** Stable user key for a connection (powers `cluster.byUser`, `isOnline`, and `toUser`). */
238
+ identify?: (conn: Conn) => string | undefined;
239
+ /** Extra fields merged into the connection's cluster descriptor (e.g. `{ plan }`). `ctx` is never auto-serialized. */
240
+ describeConn?: (conn: Conn) => Record<string, unknown>;
146
241
  /** Runs on each client subscribe. Return false or throw to deny. */
147
242
  authorizeSubscribe?: (topic: string, ctx: CtxUnion<A>, conn: Conn) => Awaitable<boolean | void>;
148
243
  /** Middleware chain run before req/subscribe handlers (rate-limit, authz, logging, metrics). */
149
244
  use?: Middleware<A>[];
245
+ /**
246
+ * Heartbeat: one timer pings every connection each `interval` ms (updating
247
+ * `conn.lastPingAt`/`lastPongAt`). Set `maxMissed` to terminate a connection
248
+ * that misses that many consecutive pongs. `false` disables it.
249
+ * Defaults to `{ interval: 30_000 }` (no reaping).
250
+ */
251
+ heartbeat?: {
252
+ interval?: number;
253
+ maxMissed?: number;
254
+ } | false;
255
+ /** Guard against slow consumers: when a connection's send buffer exceeds the limit, close or drop. */
256
+ backpressure?: Backpressure;
257
+ /**
258
+ * Read-only Control Center inspector channel, reached via the WS subprotocol
259
+ * `superline.inspector.v1`. Connections on it bypass `authenticate` and are kept out of
260
+ * presence, the heartbeat, and `local`/`cluster` results. **Default off; dev / trusted-network only.**
261
+ */
262
+ inspector?: boolean | {
263
+ redact?: string[];
264
+ };
150
265
  /** Called once per accepted connection. */
151
266
  onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
152
267
  /** Called when a connection closes, with the WebSocket close `code`. */
@@ -154,35 +269,52 @@ interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
154
269
  /** Called for any error thrown in middleware/handlers (after the client is replied to). */
155
270
  onError?: (error: unknown, info: MiddlewareInfo) => void;
156
271
  }
157
- /** A running super-line server, returned by {@link createSocketServer}. */
158
- interface SocketServer<C extends Contract, A extends AuthResult<C>> {
272
+ /** A running super-line server, returned by {@link createSuperLineServer}. */
273
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
274
+ /** This node's stable id (unique per server process). */
275
+ readonly nodeId: string;
276
+ /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
277
+ readonly nodeName: string;
278
+ /** Synchronous, node-local introspection (connections, rooms, topics on this process). */
279
+ readonly local: LocalView;
280
+ /** Asynchronous, cluster-wide introspection backed by the adapter's presence directory. */
281
+ readonly cluster: ClusterView;
282
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
283
+ isOnline(userId: string): Promise<boolean>;
284
+ /** Target a single connection by id, on whatever node holds it (cross-node emit/close). */
285
+ toConn(id: string): ConnTarget<C>;
286
+ /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
287
+ toUser(userId: string): UserTarget<C>;
159
288
  /** Register handlers for shared + per-role requests (chainable). */
160
- implement(handlers: Handlers<C, A>): SocketServer<C, A>;
289
+ implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
161
290
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
162
291
  room(name: string): Room<C>;
163
292
  /** Publish a SHARED topic to all subscribers (server-only publish). */
164
293
  publish<T extends keyof SharedTopics<C>>(topic: T, data: EmitData<SharedTopics<C>[T]>): void;
294
+ /**
295
+ * Subscribe SERVER-side to a shared topic, cluster-wide. The callback fires for a
296
+ * publish from any node — including this one (local echo, delivered in-process with
297
+ * no round-trip). `meta.from` is the publishing node; self-exclude with
298
+ * `if (meta.from === srv.nodeId) return`. Returns an unsubscribe fn.
299
+ */
300
+ subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
165
301
  /** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
166
302
  forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
167
- /** Broadcast a typed event to all OTHER server nodes (at-most-once, excludes self). */
168
- emitServer<E extends keyof ServerEvents<C>>(event: E, data: ServerEmit<ServerEvents<C>[E]>): void;
169
- /** Listen for inter-server events from other nodes. Returns an unsubscribe fn. */
170
- onServer<E extends keyof ServerEvents<C>>(event: E, handler: (data: ServerData<ServerEvents<C>[E]>) => void): () => void;
171
303
  close(): Promise<void>;
172
304
  }
173
305
  /**
174
306
  * Create a server bound to a contract. Attach it to an `http.Server`, then call
175
- * {@link SocketServer.implement} with your handlers. `authenticate` resolves each
307
+ * {@link SuperLineServer.implement} with your handlers. `authenticate` resolves each
176
308
  * connection's `{ role, ctx }` at the upgrade.
177
309
  *
178
310
  * @param contract - the shared contract.
179
311
  * @param opts - server options; `authenticate` is required.
180
- * @returns the {@link SocketServer}.
181
- * @throws nothing directly; handler throws become a typed `SocketError` to the client.
312
+ * @returns the {@link SuperLineServer}.
313
+ * @throws nothing directly; handler throws become a typed `SuperLineError` to the client.
182
314
  *
183
315
  * @example
184
316
  * ```ts
185
- * const srv = createSocketServer(api, {
317
+ * const srv = createSuperLineServer(api, {
186
318
  * server,
187
319
  * authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }),
188
320
  * })
@@ -192,6 +324,6 @@ interface SocketServer<C extends Contract, A extends AuthResult<C>> {
192
324
  * })
193
325
  * ```
194
326
  */
195
- declare function createSocketServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: ServerOptions<C, A>): SocketServer<C, A>;
327
+ declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
196
328
 
197
- export { type AuthResult, Conn, type Handlers, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerOptions, type SocketServer, createInMemoryAdapter, createSocketServer };
329
+ export { type AuthResult, type Backpressure, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import { Server, IncomingMessage } from 'node:http';
2
- import { ServerMessageDef, Serializer, ServerFrame, EmitData, Adapter, Contract, RoleOf, SharedRequests, ServerInput, SharedEvents, Output, RoleRequests, Events, RoleTopics, SharedTopics, ServerEvents, ServerEmit, ServerData } from '@super-line/core';
2
+ import { ServerMessageDef, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData } from '@super-line/core';
3
3
  import { WebSocket } from 'ws';
4
4
 
5
+ /** Backpressure policy: what to do when a connection's send buffer grows too large. */
6
+ interface Backpressure {
7
+ /** Buffer size (bytes) above which {@link Backpressure.onExceed} kicks in. */
8
+ maxBufferedBytes: number;
9
+ /** `'close'` (default) drops the connection with code 1013; `'drop'` skips the frame. */
10
+ onExceed?: 'close' | 'drop';
11
+ }
5
12
  /**
6
13
  * A single client connection, passed to handlers as the third argument.
7
14
  *
@@ -10,23 +17,43 @@ import { WebSocket } from 'ws';
10
17
  * (use a per-user room instead). Generic over the events it may emit (scoped by
11
18
  * role), its `ctx`, and its `role`.
12
19
  */
13
- declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string> {
20
+ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string, Data = unknown> {
14
21
  /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
15
22
  readonly ws: WebSocket;
23
+ /** Server-assigned unique id for this connection (stable for its lifetime). */
24
+ readonly id: string;
16
25
  /** This connection's role (the literal resolved by `authenticate`). */
17
26
  readonly role: Role;
18
27
  /** The context `authenticate` returned for this connection. */
19
28
  readonly ctx: Ctx;
20
29
  private readonly serializer;
30
+ private readonly backpressure?;
31
+ /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
32
+ private readonly onEmit?;
21
33
  /** Namespaced channels (rooms + topics) this connection belongs to. */
22
34
  readonly channels: Set<string>;
35
+ /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
+ data: Data;
37
+ /** When this connection was accepted (`Date.now()` at the upgrade). */
38
+ readonly connectedAt: number;
39
+ /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
+ lastPingAt?: number;
41
+ /** When a heartbeat pong was last received — liveness signal (managed by the server). */
42
+ lastPongAt?: number;
43
+ /** Pings sent since the last pong; drives reaping (managed by the server). */
44
+ missedPongs: number;
23
45
  constructor(
24
46
  /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
25
47
  ws: WebSocket,
48
+ /** Server-assigned unique id for this connection (stable for its lifetime). */
49
+ id: string,
26
50
  /** This connection's role (the literal resolved by `authenticate`). */
27
51
  role: Role,
28
52
  /** The context `authenticate` returned for this connection. */
29
- ctx: Ctx, serializer: Serializer);
53
+ ctx: Ctx, serializer: Serializer, backpressure?: Backpressure | undefined,
54
+ /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
55
+ onEmit?: ((event: string, data: unknown) => void) | undefined);
56
+ private overBackpressure;
30
57
  /** Encode and send a frame (unicast, e.g. req/res). */
31
58
  send(frame: ServerFrame): void;
32
59
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
@@ -40,9 +67,12 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
40
67
  /**
41
68
  * In-process pub/sub bus. Share one bus across multiple servers to simulate
42
69
  * multiple nodes in a test (each server gets its own adapter bound to the bus).
70
+ * The presence directory also lives here, so servers sharing a bus see the whole
71
+ * cluster (mirroring how Redis is shared in production).
43
72
  */
44
73
  declare class MemoryBus {
45
74
  private readonly channels;
75
+ readonly descriptors: Map<string, ConnDescriptor>;
46
76
  subscribe(channel: string, adapter: MemoryAdapter): void;
47
77
  unsubscribe(channel: string, adapter: MemoryAdapter): void;
48
78
  publish(channel: string, payload: string | Uint8Array): void;
@@ -50,6 +80,7 @@ declare class MemoryBus {
50
80
  declare class MemoryAdapter implements Adapter {
51
81
  private readonly bus;
52
82
  private handler?;
83
+ readonly presence: PresenceStore;
53
84
  constructor(bus: MemoryBus);
54
85
  subscribe(channel: string): void;
55
86
  unsubscribe(channel: string): void;
@@ -83,10 +114,10 @@ type CtxUnion<A> = A extends {
83
114
  ctx: infer X;
84
115
  } ? X : never;
85
116
  type RoleHandlers<C extends Contract, A, R extends RoleOf<C>> = {
86
- [K in keyof RoleRequests<C, R>]: (input: ServerInput<RoleRequests<C, R>[K]>, ctx: CtxFor<A, R>, conn: Conn<Events<C, R>, CtxFor<A, R>, R>) => Awaitable<Output<RoleRequests<C, R>[K]>>;
117
+ [K in keyof RoleRequests<C, R>]: (input: ServerInput<RoleRequests<C, R>[K]>, ctx: CtxFor<A, R>, conn: Conn<Events<C, R>, CtxFor<A, R>, R, DataOf<C, R>>) => Awaitable<Output<RoleRequests<C, R>[K]>>;
87
118
  };
88
119
  type SharedHandlers<C extends Contract, A> = {
89
- [K in keyof SharedRequests<C>]: (input: ServerInput<SharedRequests<C>[K]>, ctx: CtxUnion<A>, conn: Conn<SharedEvents<C>, CtxUnion<A>, RoleOf<C>>) => Awaitable<Output<SharedRequests<C>[K]>>;
120
+ [K in keyof SharedRequests<C>]: (input: ServerInput<SharedRequests<C>[K]>, ctx: CtxUnion<A>, conn: Conn<SharedEvents<C>, CtxUnion<A>, RoleOf<C>, AnyData<C>>) => Awaitable<Output<SharedRequests<C>[K]>>;
90
121
  };
91
122
  /**
92
123
  * The handler map passed to `implement`: one block per role plus an optional
@@ -100,12 +131,17 @@ type Handlers<C extends Contract, A> = ([keyof SharedRequests<C>] extends [never
100
131
  };
101
132
  /** Context passed to middleware and lifecycle hooks about the current operation. */
102
133
  interface MiddlewareInfo {
103
- /** Whether this is a request or a topic subscribe. */
104
- kind: 'request' | 'subscribe';
105
- /** The request/topic name. */
134
+ /** Whether this is a request, a topic subscribe, or a bus event delivery. */
135
+ kind: 'request' | 'subscribe' | 'event';
136
+ /** The request/topic/event name. */
106
137
  name: string;
107
- /** The connection the operation is on (`conn.role` available). */
108
- conn: Conn;
138
+ /** The connection the operation is on, if any (`conn.role` available). Absent for bus events. */
139
+ conn?: Conn;
140
+ }
141
+ /** Metadata passed to a {@link SuperLineServer.subscribe} callback alongside the event payload. */
142
+ interface BusMeta {
143
+ /** The node that published the event. Equals `srv.nodeId` for a same-node publish (local echo). */
144
+ from: string;
109
145
  }
110
146
  /**
111
147
  * Flat middleware run before request/subscribe handlers. Call `next()` to proceed,
@@ -125,14 +161,64 @@ interface Room<C extends Contract> {
125
161
  broadcast<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
126
162
  /** Member count **on the current node** (membership is node-local). */
127
163
  readonly size: number;
164
+ /** Snapshot of this room's members **on the current node**. */
165
+ readonly connections: Conn[];
166
+ }
167
+ /** Synchronous, node-local introspection of the current server process. */
168
+ interface LocalView {
169
+ /** Snapshot of all connections accepted on this node. */
170
+ readonly connections: Conn[];
171
+ /** Names of rooms with at least one member on this node. */
172
+ readonly rooms: string[];
173
+ /** Names of topics with at least one subscriber on this node. */
174
+ readonly topics: string[];
175
+ }
176
+ /**
177
+ * Asynchronous, cluster-wide introspection backed by the adapter's presence
178
+ * directory. Methods reject if the configured adapter has no presence support.
179
+ */
180
+ interface ClusterView {
181
+ /** Every live connection across the cluster. */
182
+ connections(): Promise<ConnDescriptor[]>;
183
+ /** Total live connection count across the cluster. */
184
+ count(): Promise<number>;
185
+ /** Connections for a given user key (the `identify` hook). */
186
+ byUser(userId: string): Promise<ConnDescriptor[]>;
187
+ /** Connections that are members of `room`, across nodes. */
188
+ room(name: string): Promise<ConnDescriptor[]>;
189
+ /** Per-node aggregates (the other nodes and their counts). */
190
+ topology(): Promise<NodeStat[]>;
191
+ }
192
+ /** A single targeted connection, reachable on whatever node holds it. */
193
+ interface ConnTarget<C extends Contract> {
194
+ /** Push a shared event to this connection (cross-node). */
195
+ emit<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
196
+ /**
197
+ * Send a shared server→client request and await the client's typed reply
198
+ * (cross-node). Rejects with a `TIMEOUT` `SuperLineError` if no live node owns
199
+ * the connection or the client doesn't answer in time.
200
+ */
201
+ request<M extends keyof SharedServerRequests<C>>(name: M, input: ClientInput<SharedServerRequests<C>[M]>, opts?: {
202
+ timeout?: number;
203
+ signal?: AbortSignal;
204
+ }): Promise<Output<SharedServerRequests<C>[M]>>;
205
+ /** Close this connection (cross-node kick). */
206
+ close(): void;
207
+ }
208
+ /** All of a user's connections (0..N devices), reachable across nodes. */
209
+ interface UserTarget<C extends Contract> {
210
+ /** Push a shared event to every one of the user's connections (cross-node). */
211
+ emit<E extends keyof SharedEvents<C>>(event: E, data: EmitData<SharedEvents<C>[E]>): void;
212
+ /** Disconnect every one of the user's connections (cross-node). */
213
+ disconnect(): void;
128
214
  }
129
215
  /** Lens for role-scoped server sends, returned by `srv.forRole(role)`. */
130
216
  interface RoleLens<C extends Contract, R extends RoleOf<C>> {
131
217
  /** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
132
218
  publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
133
219
  }
134
- /** Options for {@link createSocketServer}. */
135
- interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
220
+ /** Options for {@link createSuperLineServer}. */
221
+ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
136
222
  /** The `http.Server` to attach to (compose with Express/Fastify/Hono). */
137
223
  server?: Server;
138
224
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
@@ -141,12 +227,41 @@ interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
141
227
  adapter?: Adapter;
142
228
  /** Only handle upgrades for this pathname; others are left untouched. */
143
229
  path?: string;
230
+ /**
231
+ * Friendly name for this node, surfaced in `srv.nodeName`, the cluster descriptor, and the
232
+ * Control Center topology. Defaults to `SUPER_LINE_NODE_NAME` or a short slice of `nodeId`.
233
+ */
234
+ nodeName?: string;
144
235
  /** Runs at the HTTP upgrade. Return { role, ctx }, or throw to reject with 401. */
145
236
  authenticate: (req: IncomingMessage) => Awaitable<A>;
237
+ /** Stable user key for a connection (powers `cluster.byUser`, `isOnline`, and `toUser`). */
238
+ identify?: (conn: Conn) => string | undefined;
239
+ /** Extra fields merged into the connection's cluster descriptor (e.g. `{ plan }`). `ctx` is never auto-serialized. */
240
+ describeConn?: (conn: Conn) => Record<string, unknown>;
146
241
  /** Runs on each client subscribe. Return false or throw to deny. */
147
242
  authorizeSubscribe?: (topic: string, ctx: CtxUnion<A>, conn: Conn) => Awaitable<boolean | void>;
148
243
  /** Middleware chain run before req/subscribe handlers (rate-limit, authz, logging, metrics). */
149
244
  use?: Middleware<A>[];
245
+ /**
246
+ * Heartbeat: one timer pings every connection each `interval` ms (updating
247
+ * `conn.lastPingAt`/`lastPongAt`). Set `maxMissed` to terminate a connection
248
+ * that misses that many consecutive pongs. `false` disables it.
249
+ * Defaults to `{ interval: 30_000 }` (no reaping).
250
+ */
251
+ heartbeat?: {
252
+ interval?: number;
253
+ maxMissed?: number;
254
+ } | false;
255
+ /** Guard against slow consumers: when a connection's send buffer exceeds the limit, close or drop. */
256
+ backpressure?: Backpressure;
257
+ /**
258
+ * Read-only Control Center inspector channel, reached via the WS subprotocol
259
+ * `superline.inspector.v1`. Connections on it bypass `authenticate` and are kept out of
260
+ * presence, the heartbeat, and `local`/`cluster` results. **Default off; dev / trusted-network only.**
261
+ */
262
+ inspector?: boolean | {
263
+ redact?: string[];
264
+ };
150
265
  /** Called once per accepted connection. */
151
266
  onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
152
267
  /** Called when a connection closes, with the WebSocket close `code`. */
@@ -154,35 +269,52 @@ interface ServerOptions<C extends Contract, A extends AuthResult<C>> {
154
269
  /** Called for any error thrown in middleware/handlers (after the client is replied to). */
155
270
  onError?: (error: unknown, info: MiddlewareInfo) => void;
156
271
  }
157
- /** A running super-line server, returned by {@link createSocketServer}. */
158
- interface SocketServer<C extends Contract, A extends AuthResult<C>> {
272
+ /** A running super-line server, returned by {@link createSuperLineServer}. */
273
+ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
274
+ /** This node's stable id (unique per server process). */
275
+ readonly nodeId: string;
276
+ /** This node's friendly name (from `nodeName`/`SUPER_LINE_NODE_NAME`, else a short `nodeId` slice). */
277
+ readonly nodeName: string;
278
+ /** Synchronous, node-local introspection (connections, rooms, topics on this process). */
279
+ readonly local: LocalView;
280
+ /** Asynchronous, cluster-wide introspection backed by the adapter's presence directory. */
281
+ readonly cluster: ClusterView;
282
+ /** Whether a user (by `identify` key) has at least one live connection anywhere. */
283
+ isOnline(userId: string): Promise<boolean>;
284
+ /** Target a single connection by id, on whatever node holds it (cross-node emit/close). */
285
+ toConn(id: string): ConnTarget<C>;
286
+ /** Target all of a user's connections (by `identify` key) across nodes (emit/disconnect). */
287
+ toUser(userId: string): UserTarget<C>;
159
288
  /** Register handlers for shared + per-role requests (chainable). */
160
- implement(handlers: Handlers<C, A>): SocketServer<C, A>;
289
+ implement(handlers: Handlers<C, A>): SuperLineServer<C, A>;
161
290
  /** Mixed-role connection group; broadcast() sends a shared contract event to members. */
162
291
  room(name: string): Room<C>;
163
292
  /** Publish a SHARED topic to all subscribers (server-only publish). */
164
293
  publish<T extends keyof SharedTopics<C>>(topic: T, data: EmitData<SharedTopics<C>[T]>): void;
294
+ /**
295
+ * Subscribe SERVER-side to a shared topic, cluster-wide. The callback fires for a
296
+ * publish from any node — including this one (local echo, delivered in-process with
297
+ * no round-trip). `meta.from` is the publishing node; self-exclude with
298
+ * `if (meta.from === srv.nodeId) return`. Returns an unsubscribe fn.
299
+ */
300
+ subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
165
301
  /** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
166
302
  forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
167
- /** Broadcast a typed event to all OTHER server nodes (at-most-once, excludes self). */
168
- emitServer<E extends keyof ServerEvents<C>>(event: E, data: ServerEmit<ServerEvents<C>[E]>): void;
169
- /** Listen for inter-server events from other nodes. Returns an unsubscribe fn. */
170
- onServer<E extends keyof ServerEvents<C>>(event: E, handler: (data: ServerData<ServerEvents<C>[E]>) => void): () => void;
171
303
  close(): Promise<void>;
172
304
  }
173
305
  /**
174
306
  * Create a server bound to a contract. Attach it to an `http.Server`, then call
175
- * {@link SocketServer.implement} with your handlers. `authenticate` resolves each
307
+ * {@link SuperLineServer.implement} with your handlers. `authenticate` resolves each
176
308
  * connection's `{ role, ctx }` at the upgrade.
177
309
  *
178
310
  * @param contract - the shared contract.
179
311
  * @param opts - server options; `authenticate` is required.
180
- * @returns the {@link SocketServer}.
181
- * @throws nothing directly; handler throws become a typed `SocketError` to the client.
312
+ * @returns the {@link SuperLineServer}.
313
+ * @throws nothing directly; handler throws become a typed `SuperLineError` to the client.
182
314
  *
183
315
  * @example
184
316
  * ```ts
185
- * const srv = createSocketServer(api, {
317
+ * const srv = createSuperLineServer(api, {
186
318
  * server,
187
319
  * authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }),
188
320
  * })
@@ -192,6 +324,6 @@ interface SocketServer<C extends Contract, A extends AuthResult<C>> {
192
324
  * })
193
325
  * ```
194
326
  */
195
- declare function createSocketServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: ServerOptions<C, A>): SocketServer<C, A>;
327
+ declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
196
328
 
197
- export { type AuthResult, Conn, type Handlers, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerOptions, type SocketServer, createInMemoryAdapter, createSocketServer };
329
+ export { type AuthResult, type Backpressure, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer };