@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.js CHANGED
@@ -1,38 +1,71 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
1
3
  // src/index.ts
2
4
  import { randomUUID } from "crypto";
3
5
  import { WebSocketServer } from "ws";
4
6
  import {
5
7
  jsonSerializer,
6
8
  validate,
7
- SocketError
9
+ SuperLineError,
10
+ INSPECTOR_SUBPROTOCOL,
11
+ INSPECTOR_ROLE,
12
+ classifyContract
8
13
  } from "@super-line/core";
9
14
 
10
15
  // src/conn.ts
11
16
  var Conn = class {
12
- constructor(ws, role, ctx, serializer) {
17
+ constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
13
18
  this.ws = ws;
19
+ this.id = id;
14
20
  this.role = role;
15
21
  this.ctx = ctx;
16
22
  this.serializer = serializer;
23
+ this.backpressure = backpressure;
24
+ this.onEmit = onEmit;
17
25
  }
18
26
  ws;
27
+ id;
19
28
  role;
20
29
  ctx;
21
30
  serializer;
31
+ backpressure;
32
+ onEmit;
22
33
  /** Namespaced channels (rooms + topics) this connection belongs to. */
23
34
  channels = /* @__PURE__ */ new Set();
35
+ /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
+ data = {};
37
+ /** When this connection was accepted (`Date.now()` at the upgrade). */
38
+ connectedAt = Date.now();
39
+ /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
+ lastPingAt;
41
+ /** When a heartbeat pong was last received — liveness signal (managed by the server). */
42
+ lastPongAt;
43
+ /** Pings sent since the last pong; drives reaping (managed by the server). */
44
+ missedPongs = 0;
45
+ // true => the frame was handled by the backpressure policy and must not be sent
46
+ overBackpressure() {
47
+ const bp = this.backpressure;
48
+ if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
49
+ if (bp.onExceed === "drop") {
50
+ console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
51
+ return true;
52
+ }
53
+ this.ws.close(1013);
54
+ return true;
55
+ }
24
56
  /** Encode and send a frame (unicast, e.g. req/res). */
25
57
  send(frame) {
26
- if (this.ws.readyState !== this.ws.OPEN) return;
58
+ if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
27
59
  this.ws.send(this.serializer.encode(frame));
28
60
  }
29
61
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
30
62
  sendRaw(payload) {
31
- if (this.ws.readyState !== this.ws.OPEN) return;
63
+ if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
32
64
  this.ws.send(payload);
33
65
  }
34
66
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
35
67
  emit(event, data) {
68
+ this.onEmit?.(String(event), data);
36
69
  this.send({ t: "evt", e: String(event), d: data });
37
70
  }
38
71
  /** Close the underlying socket. */
@@ -44,6 +77,8 @@ var Conn = class {
44
77
  // src/memory-adapter.ts
45
78
  var MemoryBus = class {
46
79
  channels = /* @__PURE__ */ new Map();
80
+ // shared presence directory; in-memory liveness = "has a descriptor" (graceful del on disconnect)
81
+ descriptors = /* @__PURE__ */ new Map();
47
82
  subscribe(channel, adapter) {
48
83
  let set = this.channels.get(channel);
49
84
  if (!set) {
@@ -64,12 +99,69 @@ var MemoryBus = class {
64
99
  for (const adapter of set) adapter.deliver(channel, payload);
65
100
  }
66
101
  };
102
+ var MemoryPresence = class {
103
+ constructor(bus) {
104
+ this.bus = bus;
105
+ }
106
+ bus;
107
+ set(d) {
108
+ this.bus.descriptors.set(d.id, d);
109
+ }
110
+ del(connId) {
111
+ this.bus.descriptors.delete(connId);
112
+ }
113
+ beat() {
114
+ }
115
+ clearNode(nodeId) {
116
+ for (const [id, d] of this.bus.descriptors) if (d.nodeId === nodeId) this.bus.descriptors.delete(id);
117
+ }
118
+ addRoom(connId, room) {
119
+ const d = this.bus.descriptors.get(connId);
120
+ if (d && !d.rooms.includes(room)) d.rooms = [...d.rooms, room];
121
+ }
122
+ removeRoom(connId, room) {
123
+ const d = this.bus.descriptors.get(connId);
124
+ if (d) d.rooms = d.rooms.filter((r) => r !== room);
125
+ }
126
+ list() {
127
+ return [...this.bus.descriptors.values()];
128
+ }
129
+ get(connId) {
130
+ return this.bus.descriptors.get(connId);
131
+ }
132
+ byUser(userId) {
133
+ return this.list().filter((d) => d.userId === userId);
134
+ }
135
+ roomMembers(room) {
136
+ return this.list().filter((d) => d.rooms.includes(room));
137
+ }
138
+ count() {
139
+ return this.bus.descriptors.size;
140
+ }
141
+ topology() {
142
+ const byNode = /* @__PURE__ */ new Map();
143
+ for (const d of this.list()) {
144
+ const set = byNode.get(d.nodeId);
145
+ if (set) set.push(d);
146
+ else byNode.set(d.nodeId, [d]);
147
+ }
148
+ return [...byNode.entries()].map(([nodeId, ds]) => ({
149
+ nodeId,
150
+ nodeName: ds[0]?.nodeName ?? nodeId,
151
+ connections: ds.length,
152
+ rooms: new Set(ds.flatMap((d) => d.rooms)).size,
153
+ alive: true
154
+ }));
155
+ }
156
+ };
67
157
  var MemoryAdapter = class {
68
158
  constructor(bus) {
69
159
  this.bus = bus;
160
+ this.presence = new MemoryPresence(bus);
70
161
  }
71
162
  bus;
72
163
  handler;
164
+ presence;
73
165
  subscribe(channel) {
74
166
  this.bus.subscribe(channel, this);
75
167
  }
@@ -93,93 +185,396 @@ function createInMemoryAdapter(bus = new MemoryBus()) {
93
185
  // src/index.ts
94
186
  var ROOM = "r:";
95
187
  var TOPIC = "t:";
96
- var S2S = "s2s";
97
- function createSocketServer(contract, opts) {
188
+ var CONN = "c:";
189
+ var USER = "u:";
190
+ var REPLY = "reply:";
191
+ var INSPECT = "i:";
192
+ function createSuperLineServer(contract, opts) {
98
193
  const c = contract;
99
194
  const serializer = opts.serializer ?? jsonSerializer;
100
195
  const adapter = opts.adapter ?? createInMemoryAdapter();
101
- const wss = new WebSocketServer({ noServer: true });
196
+ const inspectorEnabled = !!opts.inspector;
197
+ const inspectorRedact = new Set(
198
+ opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
199
+ );
200
+ const wss = new WebSocketServer({
201
+ noServer: true,
202
+ handleProtocols: (protocols) => inspectorEnabled && protocols.has(INSPECTOR_SUBPROTOCOL) ? INSPECTOR_SUBPROTOCOL : false
203
+ });
102
204
  const conns = /* @__PURE__ */ new Set();
205
+ const inspectorConns = /* @__PURE__ */ new Set();
103
206
  const members = /* @__PURE__ */ new Map();
104
- const serverListeners = /* @__PURE__ */ new Map();
207
+ const busListeners = /* @__PURE__ */ new Map();
105
208
  const instanceId = randomUUID();
209
+ const nodeName = opts.nodeName ?? process.env.SUPER_LINE_NODE_NAME ?? instanceId.slice(0, 8);
210
+ const replyChannel = REPLY + instanceId;
106
211
  let impl = {};
212
+ let closing = false;
213
+ let nextSReq = 1;
214
+ const originWaiters = /* @__PURE__ */ new Map();
215
+ const ownerRouting = /* @__PURE__ */ new Map();
216
+ function buildDescriptor(conn) {
217
+ const userId = opts.identify?.(conn);
218
+ const rooms = [];
219
+ for (const ch of conn.channels) if (ch.startsWith(ROOM)) rooms.push(ch.slice(ROOM.length));
220
+ return {
221
+ id: conn.id,
222
+ role: conn.role,
223
+ nodeId: instanceId,
224
+ nodeName,
225
+ connectedAt: conn.connectedAt,
226
+ ...userId !== void 0 ? { userId } : {},
227
+ rooms,
228
+ ...opts.describeConn?.(conn)
229
+ };
230
+ }
231
+ function presenceOrThrow() {
232
+ if (!adapter.presence) throw new Error("cluster queries require an adapter with presence support");
233
+ return adapter.presence;
234
+ }
107
235
  adapter.onMessage((channel, payload) => {
108
- if (channel === S2S) {
109
- handleServerMessage(payload);
236
+ if (channel === replyChannel) {
237
+ handleReply(payload);
238
+ return;
239
+ }
240
+ if (channel.startsWith(CONN) || channel.startsWith(USER)) {
241
+ handlePersonal(channel, payload);
110
242
  return;
111
243
  }
112
244
  const set = members.get(channel);
113
- if (!set) return;
114
- for (const conn of set) conn.sendRaw(payload);
245
+ if (set) for (const conn of set) conn.sendRaw(payload);
246
+ const busSet = busListeners.get(channel);
247
+ if (busSet) deliverBus(payload, busSet);
115
248
  });
116
- if (c.serverToServer) void adapter.subscribe(S2S);
117
- function handleServerMessage(payload) {
118
- let msg;
249
+ void adapter.subscribe(replyChannel);
250
+ function handlePersonal(channel, payload) {
251
+ const set = members.get(channel);
252
+ if (!set) return;
253
+ let env;
119
254
  try {
120
- msg = serializer.decode(payload);
255
+ env = serializer.decode(payload);
121
256
  } catch {
122
257
  return;
123
258
  }
124
- if (msg.from === instanceId) return;
125
- const set = serverListeners.get(msg.e);
126
- if (set) for (const cb of set) cb(msg.d);
259
+ if (env.p === "close") {
260
+ for (const conn of set) conn.close();
261
+ return;
262
+ }
263
+ if (env.p === "req") {
264
+ const localId = nextSReq++;
265
+ ownerRouting.set(localId, { origin: env.o, corrId: env.i, name: env.m });
266
+ for (const conn of set) conn.send({ t: "sreq", i: localId, m: env.m, d: env.d });
267
+ return;
268
+ }
269
+ const frame = serializer.encode(env.f);
270
+ for (const conn of set) conn.sendRaw(frame);
271
+ }
272
+ function handleReply(payload) {
273
+ let env;
274
+ try {
275
+ env = serializer.decode(payload);
276
+ } catch {
277
+ return;
278
+ }
279
+ const w = originWaiters.get(env.i);
280
+ if (!w) return;
281
+ originWaiters.delete(env.i);
282
+ if (w.timer) clearTimeout(w.timer);
283
+ if (env.ok) w.resolve(env.d);
284
+ else w.reject(new SuperLineError(env.code, env.m, env.d));
285
+ }
286
+ async function handleClientReply(conn, localId, result) {
287
+ const r = ownerRouting.get(localId);
288
+ if (!r) return;
289
+ ownerRouting.delete(localId);
290
+ let env;
291
+ if (result.ok) {
292
+ try {
293
+ const def = c.roles[conn.role]?.serverToClient?.[r.name] ?? c.shared?.serverToClient?.[r.name];
294
+ const out = def && "output" in def ? await validate(def.output, result.d) : result.d;
295
+ env = { i: r.corrId, ok: true, d: out };
296
+ } catch (err) {
297
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
298
+ env = { i: r.corrId, ok: false, code: e.code, m: e.message, d: e.data };
299
+ }
300
+ } else {
301
+ env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
302
+ }
303
+ if (inspectorEnabled)
304
+ emitInspectorEvent(
305
+ env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: safeSnapshot(env.d) } : {
306
+ type: "msg.serverReply",
307
+ target: conn.id,
308
+ name: r.name,
309
+ ok: false,
310
+ error: { code: env.code, message: env.m }
311
+ }
312
+ );
313
+ void adapter.publish(REPLY + r.origin, serializer.encode(env));
314
+ }
315
+ const hb = opts.heartbeat === false ? null : opts.heartbeat ?? {};
316
+ let hbTimer;
317
+ if (hb) {
318
+ hbTimer = setInterval(() => {
319
+ const now = Date.now();
320
+ void adapter.presence?.beat(instanceId);
321
+ for (const conn of conns) {
322
+ if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
323
+ conn.ws.terminate();
324
+ continue;
325
+ }
326
+ conn.missedPongs++;
327
+ conn.lastPingAt = now;
328
+ conn.ws.ping();
329
+ }
330
+ }, hb.interval ?? 3e4);
331
+ hbTimer.unref?.();
332
+ }
333
+ function emitInspectorEvent(event) {
334
+ if (!inspectorEnabled) return;
335
+ void adapter.publish(INSPECT + "events", serializer.encode({ t: "pub", c: "events", d: event }));
127
336
  }
128
337
  function joinChannel(conn, channel) {
129
338
  conn.channels.add(channel);
339
+ if (channel.startsWith(ROOM)) {
340
+ const room2 = channel.slice(ROOM.length);
341
+ void adapter.presence?.addRoom(conn.id, room2);
342
+ emitInspectorEvent({ type: "room.add", connId: conn.id, room: room2 });
343
+ } else if (channel.startsWith(TOPIC)) {
344
+ emitInspectorEvent({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
345
+ }
130
346
  const set = members.get(channel);
131
347
  if (set) {
132
348
  set.add(conn);
133
349
  return;
134
350
  }
351
+ const alreadySubscribed = busListeners.has(channel);
135
352
  members.set(channel, /* @__PURE__ */ new Set([conn]));
136
- return adapter.subscribe(channel);
353
+ if (!alreadySubscribed) return adapter.subscribe(channel);
137
354
  }
138
355
  function leaveChannel(conn, channel) {
139
356
  const set = members.get(channel);
140
357
  if (!set) return;
141
358
  set.delete(conn);
142
359
  conn.channels.delete(channel);
360
+ if (channel.startsWith(ROOM)) {
361
+ const room2 = channel.slice(ROOM.length);
362
+ void adapter.presence?.removeRoom(conn.id, room2);
363
+ emitInspectorEvent({ type: "room.remove", connId: conn.id, room: room2 });
364
+ } else if (channel.startsWith(TOPIC)) {
365
+ emitInspectorEvent({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
366
+ }
143
367
  if (set.size === 0) {
144
368
  members.delete(channel);
145
- void adapter.unsubscribe(channel);
369
+ if (!busListeners.has(channel)) void adapter.unsubscribe(channel);
146
370
  }
147
371
  }
372
+ function isTopic(name, block) {
373
+ const def = block?.serverToClient?.[name];
374
+ return !!def && typeof def === "object" && "subscribe" in def && def.subscribe === true;
375
+ }
148
376
  function topicNamespace(role, name) {
149
- if (c.roles[role]?.serverToClient?.[name]?.subscribe) return role;
150
- if (c.shared?.serverToClient?.[name]?.subscribe) return "shared";
377
+ if (isTopic(name, c.roles[role])) return role;
378
+ if (isTopic(name, c.shared)) return "shared";
151
379
  return void 0;
152
380
  }
381
+ function localRooms() {
382
+ const out = [];
383
+ for (const channel of members.keys()) if (channel.startsWith(ROOM)) out.push(channel.slice(ROOM.length));
384
+ return out;
385
+ }
386
+ function localTopics() {
387
+ const out = [];
388
+ for (const channel of members.keys()) {
389
+ if (!channel.startsWith(TOPIC)) continue;
390
+ out.push(channel.slice(channel.indexOf(":", TOPIC.length) + 1));
391
+ }
392
+ return out;
393
+ }
394
+ async function onInspectorFrame(conn, frame) {
395
+ if (frame.t === "req") await handleInspectorReq(conn, frame);
396
+ else if (frame.t === "sub") await handleInspectorSub(conn, frame);
397
+ else if (frame.t === "unsub") leaveChannel(conn, INSPECT + "events");
398
+ }
399
+ async function handleInspectorSub(conn, frame) {
400
+ if (frame.c !== "events") {
401
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
402
+ return;
403
+ }
404
+ await joinChannel(conn, INSPECT + "events");
405
+ conn.send({ t: "res", i: frame.i, d: null });
406
+ }
407
+ async function handleInspectorReq(conn, frame) {
408
+ const handler = inspectorHandlers[frame.m];
409
+ if (!handler) {
410
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
411
+ return;
412
+ }
413
+ try {
414
+ const output = await handler(frame.d, conn);
415
+ conn.send({ t: "res", i: frame.i, d: output });
416
+ } catch (err) {
417
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
418
+ conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
419
+ }
420
+ }
421
+ async function buildInspectedContract() {
422
+ let toJsonSchema;
423
+ try {
424
+ const mod = await import("./dist-S566F7HE.js");
425
+ toJsonSchema = mod.toJsonSchema;
426
+ } catch {
427
+ return classifyContract(c);
428
+ }
429
+ const schemas = /* @__PURE__ */ new Set();
430
+ classifyContract(c, (s) => {
431
+ schemas.add(s);
432
+ return void 0;
433
+ });
434
+ const converted = /* @__PURE__ */ new Map();
435
+ await Promise.all(
436
+ [...schemas].map(
437
+ (s) => toJsonSchema(s).then(
438
+ (j) => {
439
+ converted.set(s, j);
440
+ },
441
+ () => {
442
+ }
443
+ // unsupported vendor / missing per-vendor converter -> structure-only for this entry
444
+ )
445
+ )
446
+ );
447
+ return classifyContract(c, (s) => converted.get(s));
448
+ }
449
+ function safeSnapshot(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
450
+ if (value === null) return null;
451
+ const t = typeof value;
452
+ if (t === "bigint") return `${value.toString()}n`;
453
+ if (t === "function") return "[Function]";
454
+ if (t === "symbol") return value.toString();
455
+ if (t !== "object") return value;
456
+ const obj = value;
457
+ if (obj instanceof Date) return obj.toISOString();
458
+ if (seen.has(obj)) return "[Circular]";
459
+ if (depth >= 6) return "[MaxDepth]";
460
+ seen.add(obj);
461
+ try {
462
+ if (Array.isArray(obj)) return obj.slice(0, 1e3).map((v) => safeSnapshot(v, depth + 1, seen));
463
+ const ctor = Object.getPrototypeOf(obj)?.constructor?.name;
464
+ const out = {};
465
+ if (ctor && ctor !== "Object") out["#type"] = ctor;
466
+ for (const [k, v] of Object.entries(obj)) {
467
+ out[k] = inspectorRedact.has(k) ? "[Redacted]" : safeSnapshot(v, depth + 1, seen);
468
+ }
469
+ return out;
470
+ } finally {
471
+ seen.delete(obj);
472
+ }
473
+ }
474
+ const inspectorHandlers = {
475
+ getContract: () => buildInspectedContract(),
476
+ getTopology: async () => presenceOrThrow().topology(),
477
+ listConnections: async () => presenceOrThrow().list(),
478
+ getNode: async () => ({ nodeId: instanceId, nodeName, rooms: localRooms(), topics: localTopics() }),
479
+ getConn: async (input) => {
480
+ const id = input?.id;
481
+ if (!id) throw new SuperLineError("BAD_REQUEST", "getConn requires an id");
482
+ const local = [...conns].find((cn) => cn.id === id);
483
+ if (local) {
484
+ return {
485
+ descriptor: buildDescriptor(local),
486
+ ctx: safeSnapshot(local.ctx),
487
+ data: safeSnapshot(local.data),
488
+ ctxAvailable: true
489
+ };
490
+ }
491
+ const remote = await presenceOrThrow().get(id);
492
+ if (!remote) throw new SuperLineError("NOT_FOUND", `Unknown connection: ${id}`);
493
+ return { descriptor: remote, ctxAvailable: false };
494
+ }
495
+ };
153
496
  if (opts.server) {
154
497
  opts.server.on("upgrade", (req, socket, head) => {
155
498
  void handleUpgrade(req, socket, head);
156
499
  });
157
500
  }
501
+ function isInspectorRequest(req) {
502
+ if (!inspectorEnabled) return false;
503
+ const raw = req.headers["sec-websocket-protocol"];
504
+ if (!raw) return false;
505
+ const offered = (Array.isArray(raw) ? raw.join(",") : raw).split(",").map((p) => p.trim());
506
+ return offered.includes(INSPECTOR_SUBPROTOCOL);
507
+ }
158
508
  async function handleUpgrade(req, socket, head) {
159
509
  if (opts.path) {
160
510
  const { pathname } = new URL(req.url ?? "/", "http://localhost");
161
511
  if (pathname !== opts.path) return;
162
512
  }
163
- let auth;
164
- try {
165
- auth = await opts.authenticate(req);
166
- } catch {
167
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
168
- socket.destroy();
169
- return;
513
+ const inspector = isInspectorRequest(req);
514
+ let role;
515
+ let ctx;
516
+ if (inspector) {
517
+ role = INSPECTOR_ROLE;
518
+ ctx = {};
519
+ } else {
520
+ let auth;
521
+ try {
522
+ auth = await opts.authenticate(req);
523
+ } catch {
524
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
525
+ socket.destroy();
526
+ return;
527
+ }
528
+ role = auth.role;
529
+ ctx = auth.ctx;
170
530
  }
171
531
  wss.handleUpgrade(req, socket, head, (ws) => {
172
- const conn = new Conn(ws, auth.role, auth.ctx, serializer);
173
- conns.add(conn);
532
+ const connId = randomUUID();
533
+ const conn = new Conn(
534
+ ws,
535
+ connId,
536
+ role,
537
+ ctx,
538
+ serializer,
539
+ opts.backpressure,
540
+ inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
541
+ );
174
542
  ws.on("message", (data, isBinary) => {
175
543
  void onMessage(conn, data, isBinary);
176
544
  });
545
+ if (inspector) {
546
+ inspectorConns.add(conn);
547
+ ws.on("close", () => {
548
+ inspectorConns.delete(conn);
549
+ for (const channel of conn.channels) leaveChannel(conn, channel);
550
+ });
551
+ return;
552
+ }
553
+ conns.add(conn);
554
+ ws.on("pong", () => {
555
+ conn.lastPongAt = Date.now();
556
+ conn.missedPongs = 0;
557
+ });
177
558
  ws.on("close", (code) => {
178
559
  conns.delete(conn);
179
560
  for (const channel of conn.channels) leaveChannel(conn, channel);
180
- opts.onDisconnect?.(conn, auth.ctx, code);
561
+ void adapter.presence?.del(conn.id);
562
+ const goneUserId = opts.identify?.(conn);
563
+ emitInspectorEvent({
564
+ type: "disconnect",
565
+ connId: conn.id,
566
+ nodeId: instanceId,
567
+ ...goneUserId !== void 0 ? { userId: goneUserId } : {}
568
+ });
569
+ opts.onDisconnect?.(conn, ctx, code);
181
570
  });
182
- opts.onConnection?.(conn, auth.ctx);
571
+ opts.onConnection?.(conn, ctx);
572
+ const descriptor = buildDescriptor(conn);
573
+ void adapter.presence?.set(descriptor);
574
+ emitInspectorEvent({ type: "connect", descriptor });
575
+ void joinChannel(conn, CONN + conn.id);
576
+ const uid = opts.identify?.(conn);
577
+ if (uid !== void 0) void joinChannel(conn, USER + uid);
183
578
  });
184
579
  }
185
580
  async function onMessage(conn, data, isBinary) {
@@ -189,11 +584,19 @@ function createSocketServer(contract, opts) {
189
584
  } catch {
190
585
  return;
191
586
  }
587
+ if (inspectorConns.has(conn)) {
588
+ await onInspectorFrame(conn, frame);
589
+ return;
590
+ }
192
591
  if (frame.t === "req") await handleReq(conn, frame);
193
592
  else if (frame.t === "sub") await handleSub(conn, frame);
194
593
  else if (frame.t === "unsub") {
195
594
  const ns = topicNamespace(conn.role, frame.c);
196
595
  if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
596
+ } else if (frame.t === "sres") {
597
+ await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
598
+ } else if (frame.t === "serr") {
599
+ await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
197
600
  }
198
601
  }
199
602
  function runMiddleware(info, terminal) {
@@ -217,10 +620,16 @@ function createSocketServer(contract, opts) {
217
620
  });
218
621
  } catch (err) {
219
622
  opts.onError?.(err, info);
220
- if (!responded) {
221
- const e = err instanceof SocketError ? err : new SocketError("INTERNAL", "Internal server error");
222
- conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
223
- }
623
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
624
+ if (!responded) conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
625
+ if (inspectorEnabled && info.kind === "request")
626
+ emitInspectorEvent({
627
+ type: "msg.response",
628
+ connId: conn.id,
629
+ name: info.name,
630
+ ok: false,
631
+ error: { code: e.code, message: e.message }
632
+ });
224
633
  }
225
634
  }
226
635
  async function handleReq(conn, frame) {
@@ -232,7 +641,23 @@ function createSocketServer(contract, opts) {
232
641
  }
233
642
  await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
234
643
  const input = await validate(def.input, frame.d);
644
+ if (inspectorEnabled)
645
+ emitInspectorEvent({
646
+ type: "msg.request",
647
+ connId: conn.id,
648
+ role: conn.role,
649
+ name: frame.m,
650
+ input: safeSnapshot(input)
651
+ });
235
652
  const output = await handler(input, conn.ctx, conn);
653
+ if (inspectorEnabled)
654
+ emitInspectorEvent({
655
+ type: "msg.response",
656
+ connId: conn.id,
657
+ name: frame.m,
658
+ ok: true,
659
+ output: safeSnapshot(output)
660
+ });
236
661
  conn.send({ t: "res", i: frame.i, d: output });
237
662
  });
238
663
  }
@@ -245,7 +670,7 @@ function createSocketServer(contract, opts) {
245
670
  await dispatchOp(conn, frame.i, { kind: "subscribe", name: frame.c, conn }, async () => {
246
671
  if (opts.authorizeSubscribe) {
247
672
  const ok = await opts.authorizeSubscribe(frame.c, conn.ctx, conn);
248
- if (ok === false) throw new SocketError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
673
+ if (ok === false) throw new SuperLineError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
249
674
  }
250
675
  await joinChannel(conn, TOPIC + ns + ":" + frame.c);
251
676
  conn.send({ t: "res", i: frame.i, d: null });
@@ -261,17 +686,146 @@ function createSocketServer(contract, opts) {
261
686
  leaveChannel(conn, channel);
262
687
  },
263
688
  broadcast(event, data) {
689
+ if (inspectorEnabled)
690
+ emitInspectorEvent({ type: "msg.broadcast", room: name, name: String(event), data: safeSnapshot(data) });
264
691
  void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
265
692
  },
266
693
  get size() {
267
694
  return members.get(channel)?.size ?? 0;
695
+ },
696
+ get connections() {
697
+ return [...members.get(channel) ?? []];
268
698
  }
269
699
  };
270
700
  }
271
701
  function publishTo(ns, name, data) {
272
- void adapter.publish(TOPIC + ns + ":" + name, serializer.encode({ t: "pub", c: name, d: data }));
702
+ const channel = TOPIC + ns + ":" + name;
703
+ if (inspectorEnabled) emitInspectorEvent({ type: "msg.publish", topic: name, data: safeSnapshot(data) });
704
+ const busSet = busListeners.get(channel);
705
+ if (busSet) for (const cb of busSet) callBus(cb, data, instanceId, name);
706
+ void adapter.publish(channel, serializer.encode({ t: "pub", c: name, d: data, i: instanceId }));
707
+ }
708
+ function callBus(cb, data, from, name) {
709
+ try {
710
+ cb(data, { from });
711
+ } catch (err) {
712
+ opts.onError?.(err, { kind: "event", name });
713
+ }
714
+ }
715
+ function deliverBus(payload, set) {
716
+ let frame;
717
+ try {
718
+ frame = serializer.decode(payload);
719
+ } catch {
720
+ return;
721
+ }
722
+ if (frame.i === instanceId) return;
723
+ const from = frame.i ?? "";
724
+ const name = frame.c;
725
+ const def = c.shared?.serverToClient?.[name];
726
+ const schema = def && typeof def === "object" && "payload" in def ? def.payload : void 0;
727
+ void (async () => {
728
+ let data = frame.d;
729
+ if (schema) {
730
+ try {
731
+ data = await validate(schema, frame.d);
732
+ } catch (err) {
733
+ opts.onError?.(err, { kind: "event", name });
734
+ return;
735
+ }
736
+ }
737
+ for (const cb of set) callBus(cb, data, from, name);
738
+ })();
739
+ }
740
+ function personalTarget(channel) {
741
+ return {
742
+ emit(event, data) {
743
+ if (inspectorEnabled)
744
+ emitInspectorEvent({
745
+ type: "msg.event",
746
+ target: channel.slice(channel.indexOf(":") + 1),
747
+ name: event,
748
+ data: safeSnapshot(data)
749
+ });
750
+ const env = { p: "emit", f: { t: "evt", e: event, d: data } };
751
+ void adapter.publish(channel, serializer.encode(env));
752
+ },
753
+ close() {
754
+ void adapter.publish(channel, serializer.encode({ p: "close" }));
755
+ }
756
+ };
757
+ }
758
+ function requestConn(id, name, input, opts2) {
759
+ const reqId = nextSReq++;
760
+ return new Promise((resolve, reject) => {
761
+ const ms = opts2?.timeout ?? 3e4;
762
+ const timer = ms > 0 ? setTimeout(() => {
763
+ originWaiters.delete(reqId);
764
+ reject(new SuperLineError("TIMEOUT", `Request '${name}' timed out`));
765
+ }, ms) : void 0;
766
+ originWaiters.set(reqId, { resolve, reject, timer });
767
+ opts2?.signal?.addEventListener(
768
+ "abort",
769
+ () => {
770
+ if (originWaiters.delete(reqId)) {
771
+ if (timer) clearTimeout(timer);
772
+ reject(new SuperLineError("BAD_REQUEST", "Aborted"));
773
+ }
774
+ },
775
+ { once: true }
776
+ );
777
+ if (inspectorEnabled)
778
+ emitInspectorEvent({ type: "msg.serverRequest", target: id, name, input: safeSnapshot(input) });
779
+ const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
780
+ void adapter.publish(CONN + id, serializer.encode(env));
781
+ });
273
782
  }
274
783
  const api = {
784
+ nodeId: instanceId,
785
+ nodeName,
786
+ get local() {
787
+ return {
788
+ connections: [...conns],
789
+ get rooms() {
790
+ return localRooms();
791
+ },
792
+ get topics() {
793
+ return localTopics();
794
+ }
795
+ };
796
+ },
797
+ cluster: {
798
+ async connections() {
799
+ return presenceOrThrow().list();
800
+ },
801
+ async count() {
802
+ return presenceOrThrow().count();
803
+ },
804
+ async byUser(userId) {
805
+ return presenceOrThrow().byUser(userId);
806
+ },
807
+ async room(name) {
808
+ return presenceOrThrow().roomMembers(name);
809
+ },
810
+ async topology() {
811
+ return presenceOrThrow().topology();
812
+ }
813
+ },
814
+ async isOnline(userId) {
815
+ return (await presenceOrThrow().byUser(userId)).length > 0;
816
+ },
817
+ toConn(id) {
818
+ const t = personalTarget(CONN + id);
819
+ return {
820
+ emit: t.emit,
821
+ close: t.close,
822
+ request: (name, input, opts2) => requestConn(id, String(name), input, opts2)
823
+ };
824
+ },
825
+ toUser(userId) {
826
+ const t = personalTarget(USER + userId);
827
+ return { emit: t.emit, disconnect: t.close };
828
+ },
275
829
  implement(handlers) {
276
830
  impl = handlers;
277
831
  return api;
@@ -280,33 +834,40 @@ function createSocketServer(contract, opts) {
280
834
  publish(topic, data) {
281
835
  publishTo("shared", String(topic), data);
282
836
  },
283
- forRole(role) {
284
- return {
285
- publish(topic, data) {
286
- publishTo(role, String(topic), data);
287
- }
288
- };
289
- },
290
- emitServer(event, data) {
291
- void adapter.publish(S2S, serializer.encode({ from: instanceId, e: String(event), d: data }));
292
- },
293
- onServer(event, handler) {
294
- const name = String(event);
295
- let set = serverListeners.get(name);
837
+ subscribe(topic, handler) {
838
+ const channel = TOPIC + "shared:" + String(topic);
839
+ let set = busListeners.get(channel);
296
840
  if (!set) {
297
841
  set = /* @__PURE__ */ new Set();
298
- serverListeners.set(name, set);
842
+ busListeners.set(channel, set);
843
+ if (!members.has(channel)) void adapter.subscribe(channel);
299
844
  }
300
- set.add(handler);
845
+ const cb = handler;
846
+ set.add(cb);
301
847
  return () => {
302
- const current = serverListeners.get(name);
848
+ const current = busListeners.get(channel);
303
849
  if (!current) return;
304
- current.delete(handler);
305
- if (current.size === 0) serverListeners.delete(name);
850
+ current.delete(cb);
851
+ if (current.size === 0) {
852
+ busListeners.delete(channel);
853
+ if (!members.has(channel)) void adapter.unsubscribe(channel);
854
+ }
855
+ };
856
+ },
857
+ forRole(role) {
858
+ return {
859
+ publish(topic, data) {
860
+ publishTo(role, String(topic), data);
861
+ }
306
862
  };
307
863
  },
308
864
  async close() {
865
+ if (closing) return;
866
+ closing = true;
867
+ if (hbTimer) clearInterval(hbTimer);
309
868
  for (const conn of conns) conn.close();
869
+ for (const conn of inspectorConns) conn.close();
870
+ await adapter.presence?.clearNode(instanceId);
310
871
  await adapter.close?.();
311
872
  await new Promise((resolve) => {
312
873
  wss.close(() => resolve());
@@ -324,5 +885,5 @@ export {
324
885
  Conn,
325
886
  MemoryBus,
326
887
  createInMemoryAdapter,
327
- createSocketServer
888
+ createSuperLineServer
328
889
  };