@super-line/server 0.3.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/README.md CHANGED
@@ -3,18 +3,19 @@
3
3
  The server for [**super-line**](https://mertdogar.github.io/super-line/) — end-to-end typesafe WebSockets for TypeScript. Implements a shared contract over [`ws`](https://www.npmjs.com/package/ws): role-keyed handlers, rooms, topics, middleware, lifecycle hooks, and node-to-node messaging.
4
4
 
5
5
  ```bash
6
- pnpm add @super-line/core @super-line/server zod
6
+ pnpm add @super-line/core @super-line/server @super-line/transport-websocket zod
7
7
  ```
8
8
 
9
9
  ```ts
10
10
  import http from 'node:http'
11
11
  import { createSuperLineServer } from '@super-line/server'
12
+ import { webSocketServerTransport } from '@super-line/transport-websocket'
12
13
  import { api } from './contract'
13
14
 
14
15
  const server = http.createServer()
15
16
  const srv = createSuperLineServer(api, {
16
- server,
17
- authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }), // throw -> 401
17
+ transports: [webSocketServerTransport({ server })],
18
+ authenticate: (h) => ({ role: 'user' as const, ctx: { id: '1' } }), // throw -> 401
18
19
  })
19
20
 
20
21
  srv.implement({
@@ -29,7 +30,7 @@ srv.implement({
29
30
  server.listen(3000)
30
31
  ```
31
32
 
32
- Authenticate returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
33
+ Authenticate receives the `Handshake` (`{ transport, headers, query, peer?, raw }`) and returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. The wire is carried by a pluggable transport — [`@super-line/transport-websocket`](https://www.npmjs.com/package/@super-line/transport-websocket) provides the WS transport shown above; other transports (HTTP/SSE, libp2p) are available — see the Transports guide. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
33
34
 
34
35
  - 📖 Docs: <https://mertdogar.github.io/super-line/>
35
36
  - 📚 Guides: [roles & auth](https://mertdogar.github.io/super-line/guide/roles-auth), [events & rooms](https://mertdogar.github.io/super-line/guide/events-rooms)
package/dist/index.cjs CHANGED
@@ -16276,32 +16276,31 @@ __export(index_exports, {
16276
16276
  });
16277
16277
  module.exports = __toCommonJS(index_exports);
16278
16278
  var import_node_crypto = require("crypto");
16279
- var import_ws = require("ws");
16280
16279
  var import_core2 = require("@super-line/core");
16281
16280
 
16282
16281
  // src/conn.ts
16283
16282
  var Conn = class {
16284
- constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
16285
- this.ws = ws;
16283
+ constructor(raw, id, role, ctx, serializer, onEmit) {
16284
+ this.raw = raw;
16286
16285
  this.id = id;
16287
16286
  this.role = role;
16288
16287
  this.ctx = ctx;
16289
16288
  this.serializer = serializer;
16290
- this.backpressure = backpressure;
16291
16289
  this.onEmit = onEmit;
16292
16290
  }
16293
- ws;
16291
+ raw;
16294
16292
  id;
16295
16293
  role;
16296
16294
  ctx;
16297
16295
  serializer;
16298
- backpressure;
16299
16296
  onEmit;
16300
16297
  /** Namespaced channels (rooms + topics) this connection belongs to. */
16301
16298
  channels = /* @__PURE__ */ new Set();
16302
16299
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
16303
16300
  data = {};
16304
- /** When this connection was accepted (`Date.now()` at the upgrade). */
16301
+ /** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
16302
+ transport;
16303
+ /** When this connection was accepted (`Date.now()`). */
16305
16304
  connectedAt = Date.now();
16306
16305
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
16307
16306
  lastPingAt;
@@ -16309,35 +16308,28 @@ var Conn = class {
16309
16308
  lastPongAt;
16310
16309
  /** Pings sent since the last pong; drives reaping (managed by the server). */
16311
16310
  missedPongs = 0;
16312
- // true => the frame was handled by the backpressure policy and must not be sent
16313
- overBackpressure() {
16314
- const bp = this.backpressure;
16315
- if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
16316
- if (bp.onExceed === "drop") {
16317
- console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
16318
- return true;
16319
- }
16320
- this.ws.close(1013);
16321
- return true;
16322
- }
16323
16311
  /** Encode and send a frame (unicast, e.g. req/res). */
16324
16312
  send(frame) {
16325
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
16326
- this.ws.send(this.serializer.encode(frame));
16313
+ if (!this.raw.writable) return;
16314
+ this.raw.send(this.serializer.encode(frame));
16327
16315
  }
16328
16316
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
16329
16317
  sendRaw(payload) {
16330
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
16331
- this.ws.send(payload);
16318
+ if (!this.raw.writable) return;
16319
+ this.raw.send(payload);
16332
16320
  }
16333
16321
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
16334
16322
  emit(event, data) {
16335
16323
  this.onEmit?.(String(event), data);
16336
16324
  this.send({ t: "evt", e: String(event), d: data });
16337
16325
  }
16338
- /** Close the underlying socket. */
16326
+ /** Graceful close of the underlying transport connection. */
16339
16327
  close() {
16340
- this.ws.close();
16328
+ this.raw.close();
16329
+ }
16330
+ /** Hard close with no handshake — used by heartbeat reaping. */
16331
+ terminate() {
16332
+ this.raw.terminate();
16341
16333
  }
16342
16334
  };
16343
16335
 
@@ -16464,10 +16456,6 @@ function createSuperLineServer(contract, opts) {
16464
16456
  const inspectorRedact = new Set(
16465
16457
  opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
16466
16458
  );
16467
- const wss = new import_ws.WebSocketServer({
16468
- noServer: true,
16469
- handleProtocols: (protocols) => inspectorEnabled && protocols.has(import_core2.INSPECTOR_SUBPROTOCOL) ? import_core2.INSPECTOR_SUBPROTOCOL : false
16470
- });
16471
16459
  const conns = /* @__PURE__ */ new Set();
16472
16460
  const inspectorConns = /* @__PURE__ */ new Set();
16473
16461
  const members = /* @__PURE__ */ new Map();
@@ -16492,6 +16480,7 @@ function createSuperLineServer(contract, opts) {
16492
16480
  connectedAt: conn.connectedAt,
16493
16481
  ...userId !== void 0 ? { userId } : {},
16494
16482
  rooms,
16483
+ ...conn.transport !== void 0 ? { transport: conn.transport } : {},
16495
16484
  ...opts.describeConn?.(conn)
16496
16485
  };
16497
16486
  }
@@ -16587,12 +16576,12 @@ function createSuperLineServer(contract, opts) {
16587
16576
  void adapter.presence?.beat(instanceId);
16588
16577
  for (const conn of conns) {
16589
16578
  if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
16590
- conn.ws.terminate();
16579
+ conn.terminate();
16591
16580
  continue;
16592
16581
  }
16593
16582
  conn.missedPongs++;
16594
16583
  conn.lastPingAt = now;
16595
- conn.ws.ping();
16584
+ conn.send({ t: "ping" });
16596
16585
  }
16597
16586
  }, hb.interval ?? 3e4);
16598
16587
  hbTimer.unref?.();
@@ -16760,94 +16749,65 @@ function createSuperLineServer(contract, opts) {
16760
16749
  return { descriptor: remote, ctxAvailable: false };
16761
16750
  }
16762
16751
  };
16763
- if (opts.server) {
16764
- opts.server.on("upgrade", (req, socket, head) => {
16765
- void handleUpgrade(req, socket, head);
16766
- });
16767
- }
16768
- function isInspectorRequest(req) {
16769
- if (!inspectorEnabled) return false;
16770
- const raw = req.headers["sec-websocket-protocol"];
16771
- if (!raw) return false;
16772
- const offered = (Array.isArray(raw) ? raw.join(",") : raw).split(",").map((p) => p.trim());
16773
- return offered.includes(import_core2.INSPECTOR_SUBPROTOCOL);
16774
- }
16775
- async function handleUpgrade(req, socket, head) {
16776
- if (opts.path) {
16777
- const { pathname } = new URL(req.url ?? "/", "http://localhost");
16778
- if (pathname !== opts.path) return;
16752
+ const authHook = async (handshake) => {
16753
+ const auth = await opts.authenticate(handshake);
16754
+ return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
16755
+ };
16756
+ function acceptConn(raw, auth) {
16757
+ const role = auth.role;
16758
+ const ctx = auth.ctx;
16759
+ const inspector = role === import_core2.INSPECTOR_ROLE;
16760
+ if (inspector && !inspectorEnabled) {
16761
+ raw.close();
16762
+ return;
16779
16763
  }
16780
- const inspector = isInspectorRequest(req);
16781
- let role;
16782
- let ctx;
16764
+ const connId = (0, import_node_crypto.randomUUID)();
16765
+ const conn = new Conn(
16766
+ raw,
16767
+ connId,
16768
+ role,
16769
+ ctx,
16770
+ serializer,
16771
+ inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
16772
+ );
16773
+ conn.transport = auth.transport;
16774
+ raw.onMessage((bytes) => {
16775
+ void onMessage(conn, bytes);
16776
+ });
16783
16777
  if (inspector) {
16784
- role = import_core2.INSPECTOR_ROLE;
16785
- ctx = {};
16786
- } else {
16787
- let auth;
16788
- try {
16789
- auth = await opts.authenticate(req);
16790
- } catch {
16791
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
16792
- socket.destroy();
16793
- return;
16794
- }
16795
- role = auth.role;
16796
- ctx = auth.ctx;
16797
- }
16798
- wss.handleUpgrade(req, socket, head, (ws) => {
16799
- const connId = (0, import_node_crypto.randomUUID)();
16800
- const conn = new Conn(
16801
- ws,
16802
- connId,
16803
- role,
16804
- ctx,
16805
- serializer,
16806
- opts.backpressure,
16807
- inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
16808
- );
16809
- ws.on("message", (data, isBinary) => {
16810
- void onMessage(conn, data, isBinary);
16811
- });
16812
- if (inspector) {
16813
- inspectorConns.add(conn);
16814
- ws.on("close", () => {
16815
- inspectorConns.delete(conn);
16816
- for (const channel of conn.channels) leaveChannel(conn, channel);
16817
- });
16818
- return;
16819
- }
16820
- conns.add(conn);
16821
- ws.on("pong", () => {
16822
- conn.lastPongAt = Date.now();
16823
- conn.missedPongs = 0;
16824
- });
16825
- ws.on("close", (code) => {
16826
- conns.delete(conn);
16778
+ inspectorConns.add(conn);
16779
+ raw.onClose(() => {
16780
+ inspectorConns.delete(conn);
16827
16781
  for (const channel of conn.channels) leaveChannel(conn, channel);
16828
- void adapter.presence?.del(conn.id);
16829
- const goneUserId = opts.identify?.(conn);
16830
- emitInspectorEvent({
16831
- type: "disconnect",
16832
- connId: conn.id,
16833
- nodeId: instanceId,
16834
- ...goneUserId !== void 0 ? { userId: goneUserId } : {}
16835
- });
16836
- opts.onDisconnect?.(conn, ctx, code);
16837
16782
  });
16838
- opts.onConnection?.(conn, ctx);
16839
- const descriptor = buildDescriptor(conn);
16840
- void adapter.presence?.set(descriptor);
16841
- emitInspectorEvent({ type: "connect", descriptor });
16842
- void joinChannel(conn, CONN + conn.id);
16843
- const uid = opts.identify?.(conn);
16844
- if (uid !== void 0) void joinChannel(conn, USER + uid);
16783
+ return;
16784
+ }
16785
+ conns.add(conn);
16786
+ raw.onClose((code) => {
16787
+ conns.delete(conn);
16788
+ for (const channel of conn.channels) leaveChannel(conn, channel);
16789
+ void adapter.presence?.del(conn.id);
16790
+ const goneUserId = opts.identify?.(conn);
16791
+ emitInspectorEvent({
16792
+ type: "disconnect",
16793
+ connId: conn.id,
16794
+ nodeId: instanceId,
16795
+ ...goneUserId !== void 0 ? { userId: goneUserId } : {}
16796
+ });
16797
+ opts.onDisconnect?.(conn, ctx, code);
16845
16798
  });
16846
- }
16847
- async function onMessage(conn, data, isBinary) {
16799
+ opts.onConnection?.(conn, ctx);
16800
+ const descriptor = buildDescriptor(conn);
16801
+ void adapter.presence?.set(descriptor);
16802
+ emitInspectorEvent({ type: "connect", descriptor });
16803
+ void joinChannel(conn, CONN + conn.id);
16804
+ const uid = opts.identify?.(conn);
16805
+ if (uid !== void 0) void joinChannel(conn, USER + uid);
16806
+ }
16807
+ async function onMessage(conn, bytes) {
16848
16808
  let frame;
16849
16809
  try {
16850
- frame = serializer.decode(toWire(data, isBinary));
16810
+ frame = serializer.decode(bytes);
16851
16811
  } catch {
16852
16812
  return;
16853
16813
  }
@@ -16864,8 +16824,14 @@ function createSuperLineServer(contract, opts) {
16864
16824
  await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
16865
16825
  } else if (frame.t === "serr") {
16866
16826
  await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
16827
+ } else if (frame.t === "pong") {
16828
+ conn.lastPongAt = Date.now();
16829
+ conn.missedPongs = 0;
16867
16830
  }
16868
16831
  }
16832
+ for (const transport of opts.transports) {
16833
+ void transport.start({ authenticate: authHook, onConnection: acceptConn });
16834
+ }
16869
16835
  function runMiddleware(info, terminal) {
16870
16836
  const chain = opts.use ?? [];
16871
16837
  let last = -1;
@@ -17136,18 +17102,11 @@ function createSuperLineServer(contract, opts) {
17136
17102
  for (const conn of inspectorConns) conn.close();
17137
17103
  await adapter.presence?.clearNode(instanceId);
17138
17104
  await adapter.close?.();
17139
- await new Promise((resolve) => {
17140
- wss.close(() => resolve());
17141
- });
17105
+ for (const transport of opts.transports) await transport.stop();
17142
17106
  }
17143
17107
  };
17144
17108
  return api;
17145
17109
  }
17146
- function toWire(data, _isBinary) {
17147
- if (Array.isArray(data)) return Buffer.concat(data);
17148
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
17149
- return data;
17150
- }
17151
17110
  // Annotate the CommonJS export names for ESM import in node:
17152
17111
  0 && (module.exports = {
17153
17112
  Conn,
package/dist/index.d.cts CHANGED
@@ -1,25 +1,16 @@
1
- import { Server, IncomingMessage } from 'node:http';
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
- import { WebSocket } from 'ws';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport, Handshake } from '@super-line/core';
4
2
 
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
- }
12
3
  /**
13
4
  * A single client connection, passed to handlers as the third argument.
14
5
  *
15
- * Node-local: `conn` objects live on the node that accepted the upgrade, so don't
6
+ * Node-local: `conn` objects live on the node that accepted the connection, so don't
16
7
  * stash one to reach a user later — cross-node delivery goes through the Adapter
17
8
  * (use a per-user room instead). Generic over the events it may emit (scoped by
18
9
  * role), its `ctx`, and its `role`.
19
10
  */
20
11
  declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string, Data = unknown> {
21
- /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
22
- readonly ws: WebSocket;
12
+ /** The underlying transport connection. `conn.terminate()` simulates a drop in tests. */
13
+ readonly raw: RawConn;
23
14
  /** Server-assigned unique id for this connection (stable for its lifetime). */
24
15
  readonly id: string;
25
16
  /** This connection's role (the literal resolved by `authenticate`). */
@@ -27,14 +18,15 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
27
18
  /** The context `authenticate` returned for this connection. */
28
19
  readonly ctx: Ctx;
29
20
  private readonly serializer;
30
- private readonly backpressure?;
31
21
  /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
32
22
  private readonly onEmit?;
33
23
  /** Namespaced channels (rooms + topics) this connection belongs to. */
34
24
  readonly channels: Set<string>;
35
25
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
26
  data: Data;
37
- /** When this connection was accepted (`Date.now()` at the upgrade). */
27
+ /** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
28
+ transport?: string;
29
+ /** When this connection was accepted (`Date.now()`). */
38
30
  readonly connectedAt: number;
39
31
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
32
  lastPingAt?: number;
@@ -43,25 +35,26 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
43
35
  /** Pings sent since the last pong; drives reaping (managed by the server). */
44
36
  missedPongs: number;
45
37
  constructor(
46
- /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
47
- ws: WebSocket,
38
+ /** The underlying transport connection. `conn.terminate()` simulates a drop in tests. */
39
+ raw: RawConn,
48
40
  /** Server-assigned unique id for this connection (stable for its lifetime). */
49
41
  id: string,
50
42
  /** This connection's role (the literal resolved by `authenticate`). */
51
43
  role: Role,
52
44
  /** The context `authenticate` returned for this connection. */
53
- ctx: Ctx, serializer: Serializer, backpressure?: Backpressure | undefined,
45
+ ctx: Ctx, serializer: Serializer,
54
46
  /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
55
47
  onEmit?: ((event: string, data: unknown) => void) | undefined);
56
- private overBackpressure;
57
48
  /** Encode and send a frame (unicast, e.g. req/res). */
58
49
  send(frame: ServerFrame): void;
59
50
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
60
51
  sendRaw(payload: string | Uint8Array): void;
61
52
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
62
53
  emit<E extends keyof Ev>(event: E, data: EmitData<Ev[E]>): void;
63
- /** Close the underlying socket. */
54
+ /** Graceful close of the underlying transport connection. */
64
55
  close(): void;
56
+ /** Hard close with no handshake — used by heartbeat reaping. */
57
+ terminate(): void;
65
58
  }
66
59
 
67
60
  /**
@@ -219,21 +212,19 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
219
212
  }
220
213
  /** Options for {@link createSuperLineServer}. */
221
214
  interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
222
- /** The `http.Server` to attach to (compose with Express/Fastify/Hono). */
223
- server?: Server;
215
+ /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
216
+ transports: ServerTransport[];
224
217
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
225
218
  serializer?: Serializer;
226
219
  /** Cross-node fan-out adapter. Defaults to a per-server in-memory adapter. */
227
220
  adapter?: Adapter;
228
- /** Only handle upgrades for this pathname; others are left untouched. */
229
- path?: string;
230
221
  /**
231
222
  * Friendly name for this node, surfaced in `srv.nodeName`, the cluster descriptor, and the
232
223
  * Control Center topology. Defaults to `SUPER_LINE_NODE_NAME` or a short slice of `nodeId`.
233
224
  */
234
225
  nodeName?: string;
235
- /** Runs at the HTTP upgrade. Return { role, ctx }, or throw to reject with 401. */
236
- authenticate: (req: IncomingMessage) => Awaitable<A>;
226
+ /** Authenticate a connection from its normalized {@link Handshake}. Return { role, ctx }, or throw to reject. */
227
+ authenticate: (handshake: Handshake) => Awaitable<A>;
237
228
  /** Stable user key for a connection (powers `cluster.byUser`, `isOnline`, and `toUser`). */
238
229
  identify?: (conn: Conn) => string | undefined;
239
230
  /** Extra fields merged into the connection's cluster descriptor (e.g. `{ plan }`). `ctx` is never auto-serialized. */
@@ -252,12 +243,10 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
252
243
  interval?: number;
253
244
  maxMissed?: number;
254
245
  } | false;
255
- /** Guard against slow consumers: when a connection's send buffer exceeds the limit, close or drop. */
256
- backpressure?: Backpressure;
257
246
  /**
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.**
247
+ * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
248
+ * clients. The WS transport must also be created with `inspector: true` to negotiate the
249
+ * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
261
250
  */
262
251
  inspector?: boolean | {
263
252
  redact?: string[];
@@ -315,8 +304,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
315
304
  * @example
316
305
  * ```ts
317
306
  * const srv = createSuperLineServer(api, {
318
- * server,
319
- * authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }),
307
+ * transports: [webSocketServerTransport({ server })],
308
+ * authenticate: (h) => ({ role: 'user' as const, ctx: { id: '1' } }),
320
309
  * })
321
310
  * srv.implement({
322
311
  * shared: { join: async ({ room }, _ctx, conn) => { srv.room(room).add(conn); return { ok: true } } },
@@ -326,4 +315,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
326
315
  */
327
316
  declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
328
317
 
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 };
318
+ export { type AuthResult, 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,25 +1,16 @@
1
- import { Server, IncomingMessage } from 'node:http';
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
- import { WebSocket } from 'ws';
1
+ import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport, Handshake } from '@super-line/core';
4
2
 
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
- }
12
3
  /**
13
4
  * A single client connection, passed to handlers as the third argument.
14
5
  *
15
- * Node-local: `conn` objects live on the node that accepted the upgrade, so don't
6
+ * Node-local: `conn` objects live on the node that accepted the connection, so don't
16
7
  * stash one to reach a user later — cross-node delivery goes through the Adapter
17
8
  * (use a per-user room instead). Generic over the events it may emit (scoped by
18
9
  * role), its `ctx`, and its `role`.
19
10
  */
20
11
  declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role extends string = string, Data = unknown> {
21
- /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
22
- readonly ws: WebSocket;
12
+ /** The underlying transport connection. `conn.terminate()` simulates a drop in tests. */
13
+ readonly raw: RawConn;
23
14
  /** Server-assigned unique id for this connection (stable for its lifetime). */
24
15
  readonly id: string;
25
16
  /** This connection's role (the literal resolved by `authenticate`). */
@@ -27,14 +18,15 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
27
18
  /** The context `authenticate` returned for this connection. */
28
19
  readonly ctx: Ctx;
29
20
  private readonly serializer;
30
- private readonly backpressure?;
31
21
  /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
32
22
  private readonly onEmit?;
33
23
  /** Namespaced channels (rooms + topics) this connection belongs to. */
34
24
  readonly channels: Set<string>;
35
25
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
26
  data: Data;
37
- /** When this connection was accepted (`Date.now()` at the upgrade). */
27
+ /** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
28
+ transport?: string;
29
+ /** When this connection was accepted (`Date.now()`). */
38
30
  readonly connectedAt: number;
39
31
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
32
  lastPingAt?: number;
@@ -43,25 +35,26 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
43
35
  /** Pings sent since the last pong; drives reaping (managed by the server). */
44
36
  missedPongs: number;
45
37
  constructor(
46
- /** The underlying `ws` socket. `conn.ws.terminate()` simulates a drop in tests. */
47
- ws: WebSocket,
38
+ /** The underlying transport connection. `conn.terminate()` simulates a drop in tests. */
39
+ raw: RawConn,
48
40
  /** Server-assigned unique id for this connection (stable for its lifetime). */
49
41
  id: string,
50
42
  /** This connection's role (the literal resolved by `authenticate`). */
51
43
  role: Role,
52
44
  /** The context `authenticate` returned for this connection. */
53
- ctx: Ctx, serializer: Serializer, backpressure?: Backpressure | undefined,
45
+ ctx: Ctx, serializer: Serializer,
54
46
  /** Optional inspector tap: called with each `emit` so the server can mirror it to inspectors. */
55
47
  onEmit?: ((event: string, data: unknown) => void) | undefined);
56
- private overBackpressure;
57
48
  /** Encode and send a frame (unicast, e.g. req/res). */
58
49
  send(frame: ServerFrame): void;
59
50
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
60
51
  sendRaw(payload: string | Uint8Array): void;
61
52
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
62
53
  emit<E extends keyof Ev>(event: E, data: EmitData<Ev[E]>): void;
63
- /** Close the underlying socket. */
54
+ /** Graceful close of the underlying transport connection. */
64
55
  close(): void;
56
+ /** Hard close with no handshake — used by heartbeat reaping. */
57
+ terminate(): void;
65
58
  }
66
59
 
67
60
  /**
@@ -219,21 +212,19 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
219
212
  }
220
213
  /** Options for {@link createSuperLineServer}. */
221
214
  interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
222
- /** The `http.Server` to attach to (compose with Express/Fastify/Hono). */
223
- server?: Server;
215
+ /** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
216
+ transports: ServerTransport[];
224
217
  /** Wire serializer; MUST match the client. Defaults to `jsonSerializer`. */
225
218
  serializer?: Serializer;
226
219
  /** Cross-node fan-out adapter. Defaults to a per-server in-memory adapter. */
227
220
  adapter?: Adapter;
228
- /** Only handle upgrades for this pathname; others are left untouched. */
229
- path?: string;
230
221
  /**
231
222
  * Friendly name for this node, surfaced in `srv.nodeName`, the cluster descriptor, and the
232
223
  * Control Center topology. Defaults to `SUPER_LINE_NODE_NAME` or a short slice of `nodeId`.
233
224
  */
234
225
  nodeName?: string;
235
- /** Runs at the HTTP upgrade. Return { role, ctx }, or throw to reject with 401. */
236
- authenticate: (req: IncomingMessage) => Awaitable<A>;
226
+ /** Authenticate a connection from its normalized {@link Handshake}. Return { role, ctx }, or throw to reject. */
227
+ authenticate: (handshake: Handshake) => Awaitable<A>;
237
228
  /** Stable user key for a connection (powers `cluster.byUser`, `isOnline`, and `toUser`). */
238
229
  identify?: (conn: Conn) => string | undefined;
239
230
  /** Extra fields merged into the connection's cluster descriptor (e.g. `{ plan }`). `ctx` is never auto-serialized. */
@@ -252,12 +243,10 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
252
243
  interval?: number;
253
244
  maxMissed?: number;
254
245
  } | false;
255
- /** Guard against slow consumers: when a connection's send buffer exceeds the limit, close or drop. */
256
- backpressure?: Backpressure;
257
246
  /**
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.**
247
+ * Enable the read-only Control Center inspector: emit `msg.*` telemetry and accept inspector
248
+ * clients. The WS transport must also be created with `inspector: true` to negotiate the
249
+ * `superline.inspector.v1` subprotocol. **Default off; dev / trusted-network only.**
261
250
  */
262
251
  inspector?: boolean | {
263
252
  redact?: string[];
@@ -315,8 +304,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
315
304
  * @example
316
305
  * ```ts
317
306
  * const srv = createSuperLineServer(api, {
318
- * server,
319
- * authenticate: (req) => ({ role: 'user' as const, ctx: { id: '1' } }),
307
+ * transports: [webSocketServerTransport({ server })],
308
+ * authenticate: (h) => ({ role: 'user' as const, ctx: { id: '1' } }),
320
309
  * })
321
310
  * srv.implement({
322
311
  * shared: { join: async ({ room }, _ctx, conn) => { srv.room(room).add(conn); return { ok: true } } },
@@ -326,4 +315,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
326
315
  */
327
316
  declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
328
317
 
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 };
318
+ export { type AuthResult, 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.js CHANGED
@@ -2,39 +2,37 @@ import "./chunk-MLKGABMK.js";
2
2
 
3
3
  // src/index.ts
4
4
  import { randomUUID } from "crypto";
5
- import { WebSocketServer } from "ws";
6
5
  import {
7
6
  jsonSerializer,
8
7
  validate,
9
8
  SuperLineError,
10
- INSPECTOR_SUBPROTOCOL,
11
9
  INSPECTOR_ROLE,
12
10
  classifyContract
13
11
  } from "@super-line/core";
14
12
 
15
13
  // src/conn.ts
16
14
  var Conn = class {
17
- constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
18
- this.ws = ws;
15
+ constructor(raw, id, role, ctx, serializer, onEmit) {
16
+ this.raw = raw;
19
17
  this.id = id;
20
18
  this.role = role;
21
19
  this.ctx = ctx;
22
20
  this.serializer = serializer;
23
- this.backpressure = backpressure;
24
21
  this.onEmit = onEmit;
25
22
  }
26
- ws;
23
+ raw;
27
24
  id;
28
25
  role;
29
26
  ctx;
30
27
  serializer;
31
- backpressure;
32
28
  onEmit;
33
29
  /** Namespaced channels (rooms + topics) this connection belongs to. */
34
30
  channels = /* @__PURE__ */ new Set();
35
31
  /** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
36
32
  data = {};
37
- /** 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()`). */
38
36
  connectedAt = Date.now();
39
37
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
38
  lastPingAt;
@@ -42,35 +40,28 @@ var Conn = class {
42
40
  lastPongAt;
43
41
  /** Pings sent since the last pong; drives reaping (managed by the server). */
44
42
  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
- }
56
43
  /** Encode and send a frame (unicast, e.g. req/res). */
57
44
  send(frame) {
58
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
59
- this.ws.send(this.serializer.encode(frame));
45
+ if (!this.raw.writable) return;
46
+ this.raw.send(this.serializer.encode(frame));
60
47
  }
61
48
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
62
49
  sendRaw(payload) {
63
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
64
- this.ws.send(payload);
50
+ if (!this.raw.writable) return;
51
+ this.raw.send(payload);
65
52
  }
66
53
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
67
54
  emit(event, data) {
68
55
  this.onEmit?.(String(event), data);
69
56
  this.send({ t: "evt", e: String(event), d: data });
70
57
  }
71
- /** Close the underlying socket. */
58
+ /** Graceful close of the underlying transport connection. */
72
59
  close() {
73
- this.ws.close();
60
+ this.raw.close();
61
+ }
62
+ /** Hard close with no handshake — used by heartbeat reaping. */
63
+ terminate() {
64
+ this.raw.terminate();
74
65
  }
75
66
  };
76
67
 
@@ -197,10 +188,6 @@ function createSuperLineServer(contract, opts) {
197
188
  const inspectorRedact = new Set(
198
189
  opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
199
190
  );
200
- const wss = new WebSocketServer({
201
- noServer: true,
202
- handleProtocols: (protocols) => inspectorEnabled && protocols.has(INSPECTOR_SUBPROTOCOL) ? INSPECTOR_SUBPROTOCOL : false
203
- });
204
191
  const conns = /* @__PURE__ */ new Set();
205
192
  const inspectorConns = /* @__PURE__ */ new Set();
206
193
  const members = /* @__PURE__ */ new Map();
@@ -225,6 +212,7 @@ function createSuperLineServer(contract, opts) {
225
212
  connectedAt: conn.connectedAt,
226
213
  ...userId !== void 0 ? { userId } : {},
227
214
  rooms,
215
+ ...conn.transport !== void 0 ? { transport: conn.transport } : {},
228
216
  ...opts.describeConn?.(conn)
229
217
  };
230
218
  }
@@ -320,12 +308,12 @@ function createSuperLineServer(contract, opts) {
320
308
  void adapter.presence?.beat(instanceId);
321
309
  for (const conn of conns) {
322
310
  if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
323
- conn.ws.terminate();
311
+ conn.terminate();
324
312
  continue;
325
313
  }
326
314
  conn.missedPongs++;
327
315
  conn.lastPingAt = now;
328
- conn.ws.ping();
316
+ conn.send({ t: "ping" });
329
317
  }
330
318
  }, hb.interval ?? 3e4);
331
319
  hbTimer.unref?.();
@@ -493,94 +481,65 @@ function createSuperLineServer(contract, opts) {
493
481
  return { descriptor: remote, ctxAvailable: false };
494
482
  }
495
483
  };
496
- if (opts.server) {
497
- opts.server.on("upgrade", (req, socket, head) => {
498
- void handleUpgrade(req, socket, head);
499
- });
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
- }
508
- async function handleUpgrade(req, socket, head) {
509
- if (opts.path) {
510
- const { pathname } = new URL(req.url ?? "/", "http://localhost");
511
- if (pathname !== opts.path) return;
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();
494
+ return;
512
495
  }
513
- const inspector = isInspectorRequest(req);
514
- let role;
515
- let ctx;
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
+ });
516
509
  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;
530
- }
531
- wss.handleUpgrade(req, socket, head, (ws) => {
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
- );
542
- ws.on("message", (data, isBinary) => {
543
- void onMessage(conn, data, isBinary);
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
- });
558
- ws.on("close", (code) => {
559
- conns.delete(conn);
510
+ inspectorConns.add(conn);
511
+ raw.onClose(() => {
512
+ inspectorConns.delete(conn);
560
513
  for (const channel of conn.channels) leaveChannel(conn, channel);
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);
570
514
  });
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);
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);
578
530
  });
579
- }
580
- 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) {
581
540
  let frame;
582
541
  try {
583
- frame = serializer.decode(toWire(data, isBinary));
542
+ frame = serializer.decode(bytes);
584
543
  } catch {
585
544
  return;
586
545
  }
@@ -597,8 +556,14 @@ function createSuperLineServer(contract, opts) {
597
556
  await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
598
557
  } else if (frame.t === "serr") {
599
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;
600
562
  }
601
563
  }
564
+ for (const transport of opts.transports) {
565
+ void transport.start({ authenticate: authHook, onConnection: acceptConn });
566
+ }
602
567
  function runMiddleware(info, terminal) {
603
568
  const chain = opts.use ?? [];
604
569
  let last = -1;
@@ -869,18 +834,11 @@ function createSuperLineServer(contract, opts) {
869
834
  for (const conn of inspectorConns) conn.close();
870
835
  await adapter.presence?.clearNode(instanceId);
871
836
  await adapter.close?.();
872
- await new Promise((resolve) => {
873
- wss.close(() => resolve());
874
- });
837
+ for (const transport of opts.transports) await transport.stop();
875
838
  }
876
839
  };
877
840
  return api;
878
841
  }
879
- function toWire(data, _isBinary) {
880
- if (Array.isArray(data)) return Buffer.concat(data);
881
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
882
- return data;
883
- }
884
842
  export {
885
843
  Conn,
886
844
  MemoryBus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@super-line/server",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Typesafe WebSocket server for super-line — rooms, topics, middleware, pluggable adapters.",
6
6
  "license": "MIT",
@@ -47,8 +47,7 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "ws": "^8.18.0",
51
- "@super-line/core": "^0.3.0"
50
+ "@super-line/core": "^0.4.0"
52
51
  },
53
52
  "devDependencies": {
54
53
  "@chainsafe/libp2p-noise": "^17.0.0",
@@ -60,18 +59,23 @@
60
59
  "@libp2p/memory": "^2.0.22",
61
60
  "@libp2p/tcp": "^11.0.22",
62
61
  "@types/ws": "^8.5.13",
62
+ "eventsource": "^3.0.2",
63
63
  "ioredis": "^5.4.2",
64
+ "ws": "^8.18.0",
64
65
  "libp2p": "^3.3.4",
65
66
  "rabbitmq-client": "^5.0.0",
66
67
  "testcontainers": "^10.13.2",
67
68
  "zeromq": "^6.0.0",
68
69
  "zod": "^3.24.1",
69
70
  "zod-to-json-schema": "^3.25.2",
70
- "@super-line/adapter-rabbitmq": "0.3.0",
71
- "@super-line/adapter-libp2p": "0.3.0",
72
- "@super-line/adapter-redis": "0.3.0",
73
- "@super-line/client": "0.3.0",
74
- "@super-line/adapter-zeromq": "0.3.0"
71
+ "@super-line/adapter-zeromq": "0.4.0",
72
+ "@super-line/adapter-libp2p": "0.4.0",
73
+ "@super-line/client": "0.4.0",
74
+ "@super-line/transport-http": "0.4.0",
75
+ "@super-line/transport-loopback": "0.4.0",
76
+ "@super-line/transport-websocket": "0.4.0",
77
+ "@super-line/adapter-rabbitmq": "0.4.0",
78
+ "@super-line/adapter-redis": "0.4.0"
75
79
  },
76
80
  "optionalDependencies": {
77
81
  "@standard-community/standard-json": "^0.3.5"