@super-line/server 0.2.0 → 0.4.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,33 +1,38 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
1
3
  // src/index.ts
2
4
  import { randomUUID } from "crypto";
3
- import { WebSocketServer } from "ws";
4
5
  import {
5
6
  jsonSerializer,
6
7
  validate,
7
- SocketError
8
+ SuperLineError,
9
+ INSPECTOR_ROLE,
10
+ classifyContract
8
11
  } from "@super-line/core";
9
12
 
10
13
  // src/conn.ts
11
14
  var Conn = class {
12
- constructor(ws, id, role, ctx, serializer, backpressure) {
13
- this.ws = ws;
15
+ constructor(raw, id, role, ctx, serializer, onEmit) {
16
+ this.raw = raw;
14
17
  this.id = id;
15
18
  this.role = role;
16
19
  this.ctx = ctx;
17
20
  this.serializer = serializer;
18
- this.backpressure = backpressure;
21
+ this.onEmit = onEmit;
19
22
  }
20
- ws;
23
+ raw;
21
24
  id;
22
25
  role;
23
26
  ctx;
24
27
  serializer;
25
- backpressure;
28
+ onEmit;
26
29
  /** Namespaced channels (rooms + topics) this connection belongs to. */
27
30
  channels = /* @__PURE__ */ new Set();
28
31
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
29
32
  data = {};
30
- /** When this connection was accepted (`Date.now()` at the upgrade). */
33
+ /** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
34
+ transport;
35
+ /** When this connection was accepted (`Date.now()`). */
31
36
  connectedAt = Date.now();
32
37
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
33
38
  lastPingAt;
@@ -35,34 +40,28 @@ var Conn = class {
35
40
  lastPongAt;
36
41
  /** Pings sent since the last pong; drives reaping (managed by the server). */
37
42
  missedPongs = 0;
38
- // true => the frame was handled by the backpressure policy and must not be sent
39
- overBackpressure() {
40
- const bp = this.backpressure;
41
- if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
42
- if (bp.onExceed === "drop") {
43
- console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
44
- return true;
45
- }
46
- this.ws.close(1013);
47
- return true;
48
- }
49
43
  /** Encode and send a frame (unicast, e.g. req/res). */
50
44
  send(frame) {
51
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
52
- this.ws.send(this.serializer.encode(frame));
45
+ if (!this.raw.writable) return;
46
+ this.raw.send(this.serializer.encode(frame));
53
47
  }
54
48
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
55
49
  sendRaw(payload) {
56
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
57
- this.ws.send(payload);
50
+ if (!this.raw.writable) return;
51
+ this.raw.send(payload);
58
52
  }
59
53
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
60
54
  emit(event, data) {
55
+ this.onEmit?.(String(event), data);
61
56
  this.send({ t: "evt", e: String(event), d: data });
62
57
  }
63
- /** Close the underlying socket. */
58
+ /** Graceful close of the underlying transport connection. */
64
59
  close() {
65
- this.ws.close();
60
+ this.raw.close();
61
+ }
62
+ /** Hard close with no handshake — used by heartbeat reaping. */
63
+ terminate() {
64
+ this.raw.terminate();
66
65
  }
67
66
  };
68
67
 
@@ -139,6 +138,7 @@ var MemoryPresence = class {
139
138
  }
140
139
  return [...byNode.entries()].map(([nodeId, ds]) => ({
141
140
  nodeId,
141
+ nodeName: ds[0]?.nodeName ?? nodeId,
142
142
  connections: ds.length,
143
143
  rooms: new Set(ds.flatMap((d) => d.rooms)).size,
144
144
  alive: true
@@ -179,16 +179,21 @@ var TOPIC = "t:";
179
179
  var CONN = "c:";
180
180
  var USER = "u:";
181
181
  var REPLY = "reply:";
182
- var S2S = "s2s";
183
- function createSocketServer(contract, opts) {
182
+ var INSPECT = "i:";
183
+ function createSuperLineServer(contract, opts) {
184
184
  const c = contract;
185
185
  const serializer = opts.serializer ?? jsonSerializer;
186
186
  const adapter = opts.adapter ?? createInMemoryAdapter();
187
- const wss = new WebSocketServer({ noServer: true });
187
+ const inspectorEnabled = !!opts.inspector;
188
+ const inspectorRedact = new Set(
189
+ opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
190
+ );
188
191
  const conns = /* @__PURE__ */ new Set();
192
+ const inspectorConns = /* @__PURE__ */ new Set();
189
193
  const members = /* @__PURE__ */ new Map();
190
- const serverListeners = /* @__PURE__ */ new Map();
194
+ const busListeners = /* @__PURE__ */ new Map();
191
195
  const instanceId = randomUUID();
196
+ const nodeName = opts.nodeName ?? process.env.SUPER_LINE_NODE_NAME ?? instanceId.slice(0, 8);
192
197
  const replyChannel = REPLY + instanceId;
193
198
  let impl = {};
194
199
  let closing = false;
@@ -203,9 +208,11 @@ function createSocketServer(contract, opts) {
203
208
  id: conn.id,
204
209
  role: conn.role,
205
210
  nodeId: instanceId,
211
+ nodeName,
206
212
  connectedAt: conn.connectedAt,
207
213
  ...userId !== void 0 ? { userId } : {},
208
214
  rooms,
215
+ ...conn.transport !== void 0 ? { transport: conn.transport } : {},
209
216
  ...opts.describeConn?.(conn)
210
217
  };
211
218
  }
@@ -214,10 +221,6 @@ function createSocketServer(contract, opts) {
214
221
  return adapter.presence;
215
222
  }
216
223
  adapter.onMessage((channel, payload) => {
217
- if (channel === S2S) {
218
- handleServerMessage(payload);
219
- return;
220
- }
221
224
  if (channel === replyChannel) {
222
225
  handleReply(payload);
223
226
  return;
@@ -227,8 +230,9 @@ function createSocketServer(contract, opts) {
227
230
  return;
228
231
  }
229
232
  const set = members.get(channel);
230
- if (!set) return;
231
- for (const conn of set) conn.sendRaw(payload);
233
+ if (set) for (const conn of set) conn.sendRaw(payload);
234
+ const busSet = busListeners.get(channel);
235
+ if (busSet) deliverBus(payload, busSet);
232
236
  });
233
237
  void adapter.subscribe(replyChannel);
234
238
  function handlePersonal(channel, payload) {
@@ -265,7 +269,7 @@ function createSocketServer(contract, opts) {
265
269
  originWaiters.delete(env.i);
266
270
  if (w.timer) clearTimeout(w.timer);
267
271
  if (env.ok) w.resolve(env.d);
268
- else w.reject(new SocketError(env.code, env.m, env.d));
272
+ else w.reject(new SuperLineError(env.code, env.m, env.d));
269
273
  }
270
274
  async function handleClientReply(conn, localId, result) {
271
275
  const r = ownerRouting.get(localId);
@@ -278,15 +282,24 @@ function createSocketServer(contract, opts) {
278
282
  const out = def && "output" in def ? await validate(def.output, result.d) : result.d;
279
283
  env = { i: r.corrId, ok: true, d: out };
280
284
  } catch (err) {
281
- const e = err instanceof SocketError ? err : new SocketError("INTERNAL", "Internal server error");
285
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
282
286
  env = { i: r.corrId, ok: false, code: e.code, m: e.message, d: e.data };
283
287
  }
284
288
  } else {
285
289
  env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
286
290
  }
291
+ if (inspectorEnabled)
292
+ emitInspectorEvent(
293
+ env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: safeSnapshot(env.d) } : {
294
+ type: "msg.serverReply",
295
+ target: conn.id,
296
+ name: r.name,
297
+ ok: false,
298
+ error: { code: env.code, message: env.m }
299
+ }
300
+ );
287
301
  void adapter.publish(REPLY + r.origin, serializer.encode(env));
288
302
  }
289
- if (c.serverToServer) void adapter.subscribe(S2S);
290
303
  const hb = opts.heartbeat === false ? null : opts.heartbeat ?? {};
291
304
  let hbTimer;
292
305
  if (hb) {
@@ -295,47 +308,53 @@ function createSocketServer(contract, opts) {
295
308
  void adapter.presence?.beat(instanceId);
296
309
  for (const conn of conns) {
297
310
  if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
298
- conn.ws.terminate();
311
+ conn.terminate();
299
312
  continue;
300
313
  }
301
314
  conn.missedPongs++;
302
315
  conn.lastPingAt = now;
303
- conn.ws.ping();
316
+ conn.send({ t: "ping" });
304
317
  }
305
318
  }, hb.interval ?? 3e4);
306
319
  hbTimer.unref?.();
307
320
  }
308
- function handleServerMessage(payload) {
309
- let msg;
310
- try {
311
- msg = serializer.decode(payload);
312
- } catch {
313
- return;
314
- }
315
- if (msg.from === instanceId) return;
316
- const set = serverListeners.get(msg.e);
317
- if (set) for (const cb of set) cb(msg.d);
321
+ function emitInspectorEvent(event) {
322
+ if (!inspectorEnabled) return;
323
+ void adapter.publish(INSPECT + "events", serializer.encode({ t: "pub", c: "events", d: event }));
318
324
  }
319
325
  function joinChannel(conn, channel) {
320
326
  conn.channels.add(channel);
321
- if (channel.startsWith(ROOM)) void adapter.presence?.addRoom(conn.id, channel.slice(ROOM.length));
327
+ if (channel.startsWith(ROOM)) {
328
+ const room2 = channel.slice(ROOM.length);
329
+ void adapter.presence?.addRoom(conn.id, room2);
330
+ emitInspectorEvent({ type: "room.add", connId: conn.id, room: room2 });
331
+ } else if (channel.startsWith(TOPIC)) {
332
+ emitInspectorEvent({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
333
+ }
322
334
  const set = members.get(channel);
323
335
  if (set) {
324
336
  set.add(conn);
325
337
  return;
326
338
  }
339
+ const alreadySubscribed = busListeners.has(channel);
327
340
  members.set(channel, /* @__PURE__ */ new Set([conn]));
328
- return adapter.subscribe(channel);
341
+ if (!alreadySubscribed) return adapter.subscribe(channel);
329
342
  }
330
343
  function leaveChannel(conn, channel) {
331
344
  const set = members.get(channel);
332
345
  if (!set) return;
333
346
  set.delete(conn);
334
347
  conn.channels.delete(channel);
335
- if (channel.startsWith(ROOM)) void adapter.presence?.removeRoom(conn.id, channel.slice(ROOM.length));
348
+ if (channel.startsWith(ROOM)) {
349
+ const room2 = channel.slice(ROOM.length);
350
+ void adapter.presence?.removeRoom(conn.id, room2);
351
+ emitInspectorEvent({ type: "room.remove", connId: conn.id, room: room2 });
352
+ } else if (channel.startsWith(TOPIC)) {
353
+ emitInspectorEvent({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
354
+ }
336
355
  if (set.size === 0) {
337
356
  members.delete(channel);
338
- void adapter.unsubscribe(channel);
357
+ if (!busListeners.has(channel)) void adapter.unsubscribe(channel);
339
358
  }
340
359
  }
341
360
  function isTopic(name, block) {
@@ -347,54 +366,187 @@ function createSocketServer(contract, opts) {
347
366
  if (isTopic(name, c.shared)) return "shared";
348
367
  return void 0;
349
368
  }
350
- if (opts.server) {
351
- opts.server.on("upgrade", (req, socket, head) => {
352
- void handleUpgrade(req, socket, head);
353
- });
369
+ function localRooms() {
370
+ const out = [];
371
+ for (const channel of members.keys()) if (channel.startsWith(ROOM)) out.push(channel.slice(ROOM.length));
372
+ return out;
373
+ }
374
+ function localTopics() {
375
+ const out = [];
376
+ for (const channel of members.keys()) {
377
+ if (!channel.startsWith(TOPIC)) continue;
378
+ out.push(channel.slice(channel.indexOf(":", TOPIC.length) + 1));
379
+ }
380
+ return out;
381
+ }
382
+ async function onInspectorFrame(conn, frame) {
383
+ if (frame.t === "req") await handleInspectorReq(conn, frame);
384
+ else if (frame.t === "sub") await handleInspectorSub(conn, frame);
385
+ else if (frame.t === "unsub") leaveChannel(conn, INSPECT + "events");
386
+ }
387
+ async function handleInspectorSub(conn, frame) {
388
+ if (frame.c !== "events") {
389
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
390
+ return;
391
+ }
392
+ await joinChannel(conn, INSPECT + "events");
393
+ conn.send({ t: "res", i: frame.i, d: null });
354
394
  }
355
- async function handleUpgrade(req, socket, head) {
356
- if (opts.path) {
357
- const { pathname } = new URL(req.url ?? "/", "http://localhost");
358
- if (pathname !== opts.path) return;
395
+ async function handleInspectorReq(conn, frame) {
396
+ const handler = inspectorHandlers[frame.m];
397
+ if (!handler) {
398
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
399
+ return;
359
400
  }
360
- let auth;
361
401
  try {
362
- auth = await opts.authenticate(req);
402
+ const output = await handler(frame.d, conn);
403
+ conn.send({ t: "res", i: frame.i, d: output });
404
+ } catch (err) {
405
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
406
+ conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
407
+ }
408
+ }
409
+ async function buildInspectedContract() {
410
+ let toJsonSchema;
411
+ try {
412
+ const mod = await import("./dist-S566F7HE.js");
413
+ toJsonSchema = mod.toJsonSchema;
363
414
  } catch {
364
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
365
- socket.destroy();
415
+ return classifyContract(c);
416
+ }
417
+ const schemas = /* @__PURE__ */ new Set();
418
+ classifyContract(c, (s) => {
419
+ schemas.add(s);
420
+ return void 0;
421
+ });
422
+ const converted = /* @__PURE__ */ new Map();
423
+ await Promise.all(
424
+ [...schemas].map(
425
+ (s) => toJsonSchema(s).then(
426
+ (j) => {
427
+ converted.set(s, j);
428
+ },
429
+ () => {
430
+ }
431
+ // unsupported vendor / missing per-vendor converter -> structure-only for this entry
432
+ )
433
+ )
434
+ );
435
+ return classifyContract(c, (s) => converted.get(s));
436
+ }
437
+ function safeSnapshot(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
438
+ if (value === null) return null;
439
+ const t = typeof value;
440
+ if (t === "bigint") return `${value.toString()}n`;
441
+ if (t === "function") return "[Function]";
442
+ if (t === "symbol") return value.toString();
443
+ if (t !== "object") return value;
444
+ const obj = value;
445
+ if (obj instanceof Date) return obj.toISOString();
446
+ if (seen.has(obj)) return "[Circular]";
447
+ if (depth >= 6) return "[MaxDepth]";
448
+ seen.add(obj);
449
+ try {
450
+ if (Array.isArray(obj)) return obj.slice(0, 1e3).map((v) => safeSnapshot(v, depth + 1, seen));
451
+ const ctor = Object.getPrototypeOf(obj)?.constructor?.name;
452
+ const out = {};
453
+ if (ctor && ctor !== "Object") out["#type"] = ctor;
454
+ for (const [k, v] of Object.entries(obj)) {
455
+ out[k] = inspectorRedact.has(k) ? "[Redacted]" : safeSnapshot(v, depth + 1, seen);
456
+ }
457
+ return out;
458
+ } finally {
459
+ seen.delete(obj);
460
+ }
461
+ }
462
+ const inspectorHandlers = {
463
+ getContract: () => buildInspectedContract(),
464
+ getTopology: async () => presenceOrThrow().topology(),
465
+ listConnections: async () => presenceOrThrow().list(),
466
+ getNode: async () => ({ nodeId: instanceId, nodeName, rooms: localRooms(), topics: localTopics() }),
467
+ getConn: async (input) => {
468
+ const id = input?.id;
469
+ if (!id) throw new SuperLineError("BAD_REQUEST", "getConn requires an id");
470
+ const local = [...conns].find((cn) => cn.id === id);
471
+ if (local) {
472
+ return {
473
+ descriptor: buildDescriptor(local),
474
+ ctx: safeSnapshot(local.ctx),
475
+ data: safeSnapshot(local.data),
476
+ ctxAvailable: true
477
+ };
478
+ }
479
+ const remote = await presenceOrThrow().get(id);
480
+ if (!remote) throw new SuperLineError("NOT_FOUND", `Unknown connection: ${id}`);
481
+ return { descriptor: remote, ctxAvailable: false };
482
+ }
483
+ };
484
+ const authHook = async (handshake) => {
485
+ const auth = await opts.authenticate(handshake);
486
+ return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
487
+ };
488
+ function acceptConn(raw, auth) {
489
+ const role = auth.role;
490
+ const ctx = auth.ctx;
491
+ const inspector = role === INSPECTOR_ROLE;
492
+ if (inspector && !inspectorEnabled) {
493
+ raw.close();
366
494
  return;
367
495
  }
368
- wss.handleUpgrade(req, socket, head, (ws) => {
369
- const conn = new Conn(ws, randomUUID(), auth.role, auth.ctx, serializer, opts.backpressure);
370
- conns.add(conn);
371
- ws.on("pong", () => {
372
- conn.lastPongAt = Date.now();
373
- conn.missedPongs = 0;
374
- });
375
- ws.on("message", (data, isBinary) => {
376
- void onMessage(conn, data, isBinary);
377
- });
378
- ws.on("close", (code) => {
379
- conns.delete(conn);
496
+ const connId = randomUUID();
497
+ const conn = new Conn(
498
+ raw,
499
+ connId,
500
+ role,
501
+ ctx,
502
+ serializer,
503
+ inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
504
+ );
505
+ conn.transport = auth.transport;
506
+ raw.onMessage((bytes) => {
507
+ void onMessage(conn, bytes);
508
+ });
509
+ if (inspector) {
510
+ inspectorConns.add(conn);
511
+ raw.onClose(() => {
512
+ inspectorConns.delete(conn);
380
513
  for (const channel of conn.channels) leaveChannel(conn, channel);
381
- void adapter.presence?.del(conn.id);
382
- opts.onDisconnect?.(conn, auth.ctx, code);
383
514
  });
384
- opts.onConnection?.(conn, auth.ctx);
385
- void adapter.presence?.set(buildDescriptor(conn));
386
- void joinChannel(conn, CONN + conn.id);
387
- const uid = opts.identify?.(conn);
388
- if (uid !== void 0) void joinChannel(conn, USER + uid);
515
+ return;
516
+ }
517
+ conns.add(conn);
518
+ raw.onClose((code) => {
519
+ conns.delete(conn);
520
+ for (const channel of conn.channels) leaveChannel(conn, channel);
521
+ void adapter.presence?.del(conn.id);
522
+ const goneUserId = opts.identify?.(conn);
523
+ emitInspectorEvent({
524
+ type: "disconnect",
525
+ connId: conn.id,
526
+ nodeId: instanceId,
527
+ ...goneUserId !== void 0 ? { userId: goneUserId } : {}
528
+ });
529
+ opts.onDisconnect?.(conn, ctx, code);
389
530
  });
390
- }
391
- async function onMessage(conn, data, isBinary) {
531
+ opts.onConnection?.(conn, ctx);
532
+ const descriptor = buildDescriptor(conn);
533
+ void adapter.presence?.set(descriptor);
534
+ emitInspectorEvent({ type: "connect", descriptor });
535
+ void joinChannel(conn, CONN + conn.id);
536
+ const uid = opts.identify?.(conn);
537
+ if (uid !== void 0) void joinChannel(conn, USER + uid);
538
+ }
539
+ async function onMessage(conn, bytes) {
392
540
  let frame;
393
541
  try {
394
- frame = serializer.decode(toWire(data, isBinary));
542
+ frame = serializer.decode(bytes);
395
543
  } catch {
396
544
  return;
397
545
  }
546
+ if (inspectorConns.has(conn)) {
547
+ await onInspectorFrame(conn, frame);
548
+ return;
549
+ }
398
550
  if (frame.t === "req") await handleReq(conn, frame);
399
551
  else if (frame.t === "sub") await handleSub(conn, frame);
400
552
  else if (frame.t === "unsub") {
@@ -404,8 +556,14 @@ function createSocketServer(contract, opts) {
404
556
  await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
405
557
  } else if (frame.t === "serr") {
406
558
  await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
559
+ } else if (frame.t === "pong") {
560
+ conn.lastPongAt = Date.now();
561
+ conn.missedPongs = 0;
407
562
  }
408
563
  }
564
+ for (const transport of opts.transports) {
565
+ void transport.start({ authenticate: authHook, onConnection: acceptConn });
566
+ }
409
567
  function runMiddleware(info, terminal) {
410
568
  const chain = opts.use ?? [];
411
569
  let last = -1;
@@ -427,10 +585,16 @@ function createSocketServer(contract, opts) {
427
585
  });
428
586
  } catch (err) {
429
587
  opts.onError?.(err, info);
430
- if (!responded) {
431
- const e = err instanceof SocketError ? err : new SocketError("INTERNAL", "Internal server error");
432
- conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
433
- }
588
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
589
+ if (!responded) conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
590
+ if (inspectorEnabled && info.kind === "request")
591
+ emitInspectorEvent({
592
+ type: "msg.response",
593
+ connId: conn.id,
594
+ name: info.name,
595
+ ok: false,
596
+ error: { code: e.code, message: e.message }
597
+ });
434
598
  }
435
599
  }
436
600
  async function handleReq(conn, frame) {
@@ -442,7 +606,23 @@ function createSocketServer(contract, opts) {
442
606
  }
443
607
  await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
444
608
  const input = await validate(def.input, frame.d);
609
+ if (inspectorEnabled)
610
+ emitInspectorEvent({
611
+ type: "msg.request",
612
+ connId: conn.id,
613
+ role: conn.role,
614
+ name: frame.m,
615
+ input: safeSnapshot(input)
616
+ });
445
617
  const output = await handler(input, conn.ctx, conn);
618
+ if (inspectorEnabled)
619
+ emitInspectorEvent({
620
+ type: "msg.response",
621
+ connId: conn.id,
622
+ name: frame.m,
623
+ ok: true,
624
+ output: safeSnapshot(output)
625
+ });
446
626
  conn.send({ t: "res", i: frame.i, d: output });
447
627
  });
448
628
  }
@@ -455,7 +635,7 @@ function createSocketServer(contract, opts) {
455
635
  await dispatchOp(conn, frame.i, { kind: "subscribe", name: frame.c, conn }, async () => {
456
636
  if (opts.authorizeSubscribe) {
457
637
  const ok = await opts.authorizeSubscribe(frame.c, conn.ctx, conn);
458
- if (ok === false) throw new SocketError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
638
+ if (ok === false) throw new SuperLineError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
459
639
  }
460
640
  await joinChannel(conn, TOPIC + ns + ":" + frame.c);
461
641
  conn.send({ t: "res", i: frame.i, d: null });
@@ -471,6 +651,8 @@ function createSocketServer(contract, opts) {
471
651
  leaveChannel(conn, channel);
472
652
  },
473
653
  broadcast(event, data) {
654
+ if (inspectorEnabled)
655
+ emitInspectorEvent({ type: "msg.broadcast", room: name, name: String(event), data: safeSnapshot(data) });
474
656
  void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
475
657
  },
476
658
  get size() {
@@ -482,11 +664,54 @@ function createSocketServer(contract, opts) {
482
664
  };
483
665
  }
484
666
  function publishTo(ns, name, data) {
485
- void adapter.publish(TOPIC + ns + ":" + name, serializer.encode({ t: "pub", c: name, d: data }));
667
+ const channel = TOPIC + ns + ":" + name;
668
+ if (inspectorEnabled) emitInspectorEvent({ type: "msg.publish", topic: name, data: safeSnapshot(data) });
669
+ const busSet = busListeners.get(channel);
670
+ if (busSet) for (const cb of busSet) callBus(cb, data, instanceId, name);
671
+ void adapter.publish(channel, serializer.encode({ t: "pub", c: name, d: data, i: instanceId }));
672
+ }
673
+ function callBus(cb, data, from, name) {
674
+ try {
675
+ cb(data, { from });
676
+ } catch (err) {
677
+ opts.onError?.(err, { kind: "event", name });
678
+ }
679
+ }
680
+ function deliverBus(payload, set) {
681
+ let frame;
682
+ try {
683
+ frame = serializer.decode(payload);
684
+ } catch {
685
+ return;
686
+ }
687
+ if (frame.i === instanceId) return;
688
+ const from = frame.i ?? "";
689
+ const name = frame.c;
690
+ const def = c.shared?.serverToClient?.[name];
691
+ const schema = def && typeof def === "object" && "payload" in def ? def.payload : void 0;
692
+ void (async () => {
693
+ let data = frame.d;
694
+ if (schema) {
695
+ try {
696
+ data = await validate(schema, frame.d);
697
+ } catch (err) {
698
+ opts.onError?.(err, { kind: "event", name });
699
+ return;
700
+ }
701
+ }
702
+ for (const cb of set) callBus(cb, data, from, name);
703
+ })();
486
704
  }
487
705
  function personalTarget(channel) {
488
706
  return {
489
707
  emit(event, data) {
708
+ if (inspectorEnabled)
709
+ emitInspectorEvent({
710
+ type: "msg.event",
711
+ target: channel.slice(channel.indexOf(":") + 1),
712
+ name: event,
713
+ data: safeSnapshot(data)
714
+ });
490
715
  const env = { p: "emit", f: { t: "evt", e: event, d: data } };
491
716
  void adapter.publish(channel, serializer.encode(env));
492
717
  },
@@ -501,7 +726,7 @@ function createSocketServer(contract, opts) {
501
726
  const ms = opts2?.timeout ?? 3e4;
502
727
  const timer = ms > 0 ? setTimeout(() => {
503
728
  originWaiters.delete(reqId);
504
- reject(new SocketError("TIMEOUT", `Request '${name}' timed out`));
729
+ reject(new SuperLineError("TIMEOUT", `Request '${name}' timed out`));
505
730
  }, ms) : void 0;
506
731
  originWaiters.set(reqId, { resolve, reject, timer });
507
732
  opts2?.signal?.addEventListener(
@@ -509,32 +734,28 @@ function createSocketServer(contract, opts) {
509
734
  () => {
510
735
  if (originWaiters.delete(reqId)) {
511
736
  if (timer) clearTimeout(timer);
512
- reject(new SocketError("BAD_REQUEST", "Aborted"));
737
+ reject(new SuperLineError("BAD_REQUEST", "Aborted"));
513
738
  }
514
739
  },
515
740
  { once: true }
516
741
  );
742
+ if (inspectorEnabled)
743
+ emitInspectorEvent({ type: "msg.serverRequest", target: id, name, input: safeSnapshot(input) });
517
744
  const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
518
745
  void adapter.publish(CONN + id, serializer.encode(env));
519
746
  });
520
747
  }
521
748
  const api = {
522
749
  nodeId: instanceId,
750
+ nodeName,
523
751
  get local() {
524
752
  return {
525
753
  connections: [...conns],
526
754
  get rooms() {
527
- const out = [];
528
- for (const channel of members.keys()) if (channel.startsWith(ROOM)) out.push(channel.slice(ROOM.length));
529
- return out;
755
+ return localRooms();
530
756
  },
531
757
  get topics() {
532
- const out = [];
533
- for (const channel of members.keys()) {
534
- if (!channel.startsWith(TOPIC)) continue;
535
- out.push(channel.slice(channel.indexOf(":", TOPIC.length) + 1));
536
- }
537
- return out;
758
+ return localTopics();
538
759
  }
539
760
  };
540
761
  },
@@ -578,29 +799,31 @@ function createSocketServer(contract, opts) {
578
799
  publish(topic, data) {
579
800
  publishTo("shared", String(topic), data);
580
801
  },
581
- forRole(role) {
582
- return {
583
- publish(topic, data) {
584
- publishTo(role, String(topic), data);
585
- }
586
- };
587
- },
588
- emitServer(event, data) {
589
- void adapter.publish(S2S, serializer.encode({ from: instanceId, e: String(event), d: data }));
590
- },
591
- onServer(event, handler) {
592
- const name = String(event);
593
- let set = serverListeners.get(name);
802
+ subscribe(topic, handler) {
803
+ const channel = TOPIC + "shared:" + String(topic);
804
+ let set = busListeners.get(channel);
594
805
  if (!set) {
595
806
  set = /* @__PURE__ */ new Set();
596
- serverListeners.set(name, set);
807
+ busListeners.set(channel, set);
808
+ if (!members.has(channel)) void adapter.subscribe(channel);
597
809
  }
598
- set.add(handler);
810
+ const cb = handler;
811
+ set.add(cb);
599
812
  return () => {
600
- const current = serverListeners.get(name);
813
+ const current = busListeners.get(channel);
601
814
  if (!current) return;
602
- current.delete(handler);
603
- if (current.size === 0) serverListeners.delete(name);
815
+ current.delete(cb);
816
+ if (current.size === 0) {
817
+ busListeners.delete(channel);
818
+ if (!members.has(channel)) void adapter.unsubscribe(channel);
819
+ }
820
+ };
821
+ },
822
+ forRole(role) {
823
+ return {
824
+ publish(topic, data) {
825
+ publishTo(role, String(topic), data);
826
+ }
604
827
  };
605
828
  },
606
829
  async close() {
@@ -608,23 +831,17 @@ function createSocketServer(contract, opts) {
608
831
  closing = true;
609
832
  if (hbTimer) clearInterval(hbTimer);
610
833
  for (const conn of conns) conn.close();
834
+ for (const conn of inspectorConns) conn.close();
611
835
  await adapter.presence?.clearNode(instanceId);
612
836
  await adapter.close?.();
613
- await new Promise((resolve) => {
614
- wss.close(() => resolve());
615
- });
837
+ for (const transport of opts.transports) await transport.stop();
616
838
  }
617
839
  };
618
840
  return api;
619
841
  }
620
- function toWire(data, _isBinary) {
621
- if (Array.isArray(data)) return Buffer.concat(data);
622
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
623
- return data;
624
- }
625
842
  export {
626
843
  Conn,
627
844
  MemoryBus,
628
845
  createInMemoryAdapter,
629
- createSocketServer
846
+ createSuperLineServer
630
847
  };