@super-line/server 0.3.0 → 0.5.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
@@ -2,39 +2,39 @@ 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
+ /** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
36
+ principal;
37
+ /** When this connection was accepted (`Date.now()`). */
38
38
  connectedAt = Date.now();
39
39
  /** When the server last sent a heartbeat ping to this connection (managed by the server). */
40
40
  lastPingAt;
@@ -42,37 +42,33 @@ var Conn = class {
42
42
  lastPongAt;
43
43
  /** Pings sent since the last pong; drives reaping (managed by the server). */
44
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
- }
56
45
  /** Encode and send a frame (unicast, e.g. req/res). */
57
46
  send(frame) {
58
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
59
- this.ws.send(this.serializer.encode(frame));
47
+ if (!this.raw.writable) return;
48
+ this.raw.send(this.serializer.encode(frame));
60
49
  }
61
50
  /** Forward an already-encoded frame (fan-out path; encoded once at the source). */
62
51
  sendRaw(payload) {
63
- if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
64
- this.ws.send(payload);
52
+ if (!this.raw.writable) return;
53
+ this.raw.send(payload);
65
54
  }
66
55
  /** Push an event to THIS connection (node-local). Scoped to the role's events. */
67
56
  emit(event, data) {
68
57
  this.onEmit?.(String(event), data);
69
58
  this.send({ t: "evt", e: String(event), d: data });
70
59
  }
71
- /** Close the underlying socket. */
60
+ /** Graceful close of the underlying transport connection. */
72
61
  close() {
73
- this.ws.close();
62
+ this.raw.close();
63
+ }
64
+ /** Hard close with no handshake — used by heartbeat reaping. */
65
+ terminate() {
66
+ this.raw.terminate();
74
67
  }
75
68
  };
69
+ function resolvePrincipal(conn, identify) {
70
+ return identify?.(conn) ?? conn.id;
71
+ }
76
72
 
77
73
  // src/memory-adapter.ts
78
74
  var MemoryBus = class {
@@ -189,6 +185,8 @@ var CONN = "c:";
189
185
  var USER = "u:";
190
186
  var REPLY = "reply:";
191
187
  var INSPECT = "i:";
188
+ var STORE = "s:";
189
+ var SERVER_ORIGIN = "server";
192
190
  function createSuperLineServer(contract, opts) {
193
191
  const c = contract;
194
192
  const serializer = opts.serializer ?? jsonSerializer;
@@ -197,10 +195,7 @@ function createSuperLineServer(contract, opts) {
197
195
  const inspectorRedact = new Set(
198
196
  opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
199
197
  );
200
- const wss = new WebSocketServer({
201
- noServer: true,
202
- handleProtocols: (protocols) => inspectorEnabled && protocols.has(INSPECTOR_SUBPROTOCOL) ? INSPECTOR_SUBPROTOCOL : false
203
- });
198
+ const storeMap = opts.stores ?? {};
204
199
  const conns = /* @__PURE__ */ new Set();
205
200
  const inspectorConns = /* @__PURE__ */ new Set();
206
201
  const members = /* @__PURE__ */ new Map();
@@ -210,6 +205,7 @@ function createSuperLineServer(contract, opts) {
210
205
  const replyChannel = REPLY + instanceId;
211
206
  let impl = {};
212
207
  let closing = false;
208
+ let relaying = false;
213
209
  let nextSReq = 1;
214
210
  const originWaiters = /* @__PURE__ */ new Map();
215
211
  const ownerRouting = /* @__PURE__ */ new Map();
@@ -225,6 +221,7 @@ function createSuperLineServer(contract, opts) {
225
221
  connectedAt: conn.connectedAt,
226
222
  ...userId !== void 0 ? { userId } : {},
227
223
  rooms,
224
+ ...conn.transport !== void 0 ? { transport: conn.transport } : {},
228
225
  ...opts.describeConn?.(conn)
229
226
  };
230
227
  }
@@ -241,6 +238,10 @@ function createSuperLineServer(contract, opts) {
241
238
  handlePersonal(channel, payload);
242
239
  return;
243
240
  }
241
+ if (channel.startsWith(STORE)) {
242
+ handleStoreRelay(channel, payload);
243
+ return;
244
+ }
244
245
  const set = members.get(channel);
245
246
  if (set) for (const conn of set) conn.sendRaw(payload);
246
247
  const busSet = busListeners.get(channel);
@@ -320,12 +321,12 @@ function createSuperLineServer(contract, opts) {
320
321
  void adapter.presence?.beat(instanceId);
321
322
  for (const conn of conns) {
322
323
  if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
323
- conn.ws.terminate();
324
+ conn.terminate();
324
325
  continue;
325
326
  }
326
327
  conn.missedPongs++;
327
328
  conn.lastPingAt = now;
328
- conn.ws.ping();
329
+ conn.send({ t: "ping" });
329
330
  }
330
331
  }, hb.interval ?? 3e4);
331
332
  hbTimer.unref?.();
@@ -493,94 +494,66 @@ function createSuperLineServer(contract, opts) {
493
494
  return { descriptor: remote, ctxAvailable: false };
494
495
  }
495
496
  };
496
- if (opts.server) {
497
- opts.server.on("upgrade", (req, socket, head) => {
498
- void handleUpgrade(req, socket, head);
497
+ const authHook = async (handshake) => {
498
+ const auth = await opts.authenticate(handshake);
499
+ return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
500
+ };
501
+ function acceptConn(raw, auth) {
502
+ const role = auth.role;
503
+ const ctx = auth.ctx;
504
+ const inspector = role === INSPECTOR_ROLE;
505
+ if (inspector && !inspectorEnabled) {
506
+ raw.close();
507
+ return;
508
+ }
509
+ const connId = randomUUID();
510
+ const conn = new Conn(
511
+ raw,
512
+ connId,
513
+ role,
514
+ ctx,
515
+ serializer,
516
+ inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
517
+ );
518
+ conn.transport = auth.transport;
519
+ conn.principal = resolvePrincipal(conn, opts.identify);
520
+ raw.onMessage((bytes) => {
521
+ void onMessage(conn, bytes);
499
522
  });
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;
512
- }
513
- const inspector = isInspectorRequest(req);
514
- let role;
515
- let ctx;
516
523
  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);
524
+ inspectorConns.add(conn);
525
+ raw.onClose(() => {
526
+ inspectorConns.delete(conn);
560
527
  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
528
  });
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);
529
+ return;
530
+ }
531
+ conns.add(conn);
532
+ raw.onClose((code) => {
533
+ conns.delete(conn);
534
+ for (const channel of conn.channels) leaveChannel(conn, channel);
535
+ void adapter.presence?.del(conn.id);
536
+ const goneUserId = opts.identify?.(conn);
537
+ emitInspectorEvent({
538
+ type: "disconnect",
539
+ connId: conn.id,
540
+ nodeId: instanceId,
541
+ ...goneUserId !== void 0 ? { userId: goneUserId } : {}
542
+ });
543
+ opts.onDisconnect?.(conn, ctx, code);
578
544
  });
579
- }
580
- async function onMessage(conn, data, isBinary) {
545
+ opts.onConnection?.(conn, ctx);
546
+ const descriptor = buildDescriptor(conn);
547
+ void adapter.presence?.set(descriptor);
548
+ emitInspectorEvent({ type: "connect", descriptor });
549
+ void joinChannel(conn, CONN + conn.id);
550
+ const uid = opts.identify?.(conn);
551
+ if (uid !== void 0) void joinChannel(conn, USER + uid);
552
+ }
553
+ async function onMessage(conn, bytes) {
581
554
  let frame;
582
555
  try {
583
- frame = serializer.decode(toWire(data, isBinary));
556
+ frame = serializer.decode(bytes);
584
557
  } catch {
585
558
  return;
586
559
  }
@@ -593,12 +566,22 @@ function createSuperLineServer(contract, opts) {
593
566
  else if (frame.t === "unsub") {
594
567
  const ns = topicNamespace(conn.role, frame.c);
595
568
  if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
596
- } else if (frame.t === "sres") {
569
+ } else if (frame.t === "sopen") await handleStoreOpen(conn, frame);
570
+ else if (frame.t === "srd") await handleStoreRead(conn, frame);
571
+ else if (frame.t === "swr") await handleStoreWrite(conn, frame);
572
+ else if (frame.t === "sclose") handleStoreClose(conn, frame);
573
+ else if (frame.t === "sres") {
597
574
  await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
598
575
  } else if (frame.t === "serr") {
599
576
  await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
577
+ } else if (frame.t === "pong") {
578
+ conn.lastPongAt = Date.now();
579
+ conn.missedPongs = 0;
600
580
  }
601
581
  }
582
+ for (const transport of opts.transports) {
583
+ void transport.start({ authenticate: authHook, onConnection: acceptConn });
584
+ }
602
585
  function runMiddleware(info, terminal) {
603
586
  const chain = opts.use ?? [];
604
587
  let last = -1;
@@ -676,6 +659,100 @@ function createSuperLineServer(contract, opts) {
676
659
  conn.send({ t: "res", i: frame.i, d: null });
677
660
  });
678
661
  }
662
+ function storeOrErr(conn, id, name) {
663
+ const store = storeMap[name];
664
+ if (!store) conn.send({ t: "err", i: id, code: "NOT_FOUND", m: `Unknown store: ${name}` });
665
+ return store;
666
+ }
667
+ async function handleStoreOpen(conn, frame) {
668
+ const store = storeOrErr(conn, frame.i, frame.n);
669
+ if (!store) return;
670
+ await dispatchOp(conn, frame.i, { kind: "subscribe", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
671
+ const resource = await store.read(frame.id);
672
+ if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
673
+ const principal = conn.principal ?? conn.id;
674
+ if (!resource.accessRules[principal]?.read)
675
+ throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
676
+ await joinChannel(conn, STORE + frame.n + ":" + frame.id);
677
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
678
+ conn.send({ t: "res", i: frame.i, d: resource.data });
679
+ });
680
+ }
681
+ async function handleStoreRead(conn, frame) {
682
+ const store = storeOrErr(conn, frame.i, frame.n);
683
+ if (!store) return;
684
+ await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
685
+ const resource = await store.read(frame.id);
686
+ if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
687
+ const principal = conn.principal ?? conn.id;
688
+ if (!resource.accessRules[principal]?.read)
689
+ throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
690
+ conn.send({ t: "res", i: frame.i, d: resource.data });
691
+ });
692
+ }
693
+ async function handleStoreWrite(conn, frame) {
694
+ const store = storeOrErr(conn, frame.i, frame.n);
695
+ if (!store) return;
696
+ await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
697
+ const resource = await store.read(frame.id);
698
+ if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
699
+ const principal = conn.principal ?? conn.id;
700
+ if (!resource.accessRules[principal]?.write)
701
+ throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
702
+ await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
703
+ if (inspectorEnabled)
704
+ emitInspectorEvent({
705
+ type: "store.write",
706
+ store: frame.n,
707
+ id: frame.id,
708
+ origin: frame.o,
709
+ connId: conn.id,
710
+ data: safeSnapshot(frame.u)
711
+ });
712
+ conn.send({ t: "res", i: frame.i, d: null });
713
+ });
714
+ }
715
+ function handleStoreClose(conn, frame) {
716
+ if (!storeMap[frame.n]) return;
717
+ leaveChannel(conn, STORE + frame.n + ":" + frame.id);
718
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
719
+ }
720
+ for (const [name, store] of Object.entries(storeMap)) {
721
+ store.onChange((change) => {
722
+ if (relaying) return;
723
+ void adapter.publish(
724
+ STORE + name + ":" + change.id,
725
+ serializer.encode({
726
+ t: "sch",
727
+ n: name,
728
+ id: change.id,
729
+ u: change.update,
730
+ o: change.origin,
731
+ nd: instanceId
732
+ })
733
+ );
734
+ });
735
+ }
736
+ function handleStoreRelay(channel, payload) {
737
+ const set = members.get(channel);
738
+ if (set) for (const conn of set) conn.sendRaw(payload);
739
+ let frame;
740
+ try {
741
+ frame = serializer.decode(payload);
742
+ } catch {
743
+ return;
744
+ }
745
+ if (frame.nd === instanceId) return;
746
+ const store = storeMap[frame.n];
747
+ if (!store || store.clustering !== "relay") return;
748
+ relaying = true;
749
+ try {
750
+ void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
751
+ } catch {
752
+ } finally {
753
+ relaying = false;
754
+ }
755
+ }
679
756
  function room(name) {
680
757
  const channel = ROOM + name;
681
758
  return {
@@ -780,6 +857,45 @@ function createSuperLineServer(contract, opts) {
780
857
  void adapter.publish(CONN + id, serializer.encode(env));
781
858
  });
782
859
  }
860
+ const storeApi = {};
861
+ for (const [name, store] of Object.entries(storeMap)) {
862
+ const readOrThrow = async (id) => {
863
+ const r = await store.read(id);
864
+ if (!r) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
865
+ return r;
866
+ };
867
+ storeApi[name] = {
868
+ async create(id, data, accessRules) {
869
+ await store.create(id, data, accessRules);
870
+ },
871
+ read(id) {
872
+ return Promise.resolve(store.read(id));
873
+ },
874
+ async write(id, data) {
875
+ await store.apply({ id, update: data, origin: SERVER_ORIGIN });
876
+ if (inspectorEnabled)
877
+ emitInspectorEvent({ type: "store.write", store: name, id, origin: SERVER_ORIGIN, data: safeSnapshot(data) });
878
+ },
879
+ async grant(id, principal, perms) {
880
+ const r = await readOrThrow(id);
881
+ await store.setAccess(id, { ...r.accessRules, [principal]: perms });
882
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.grant", store: name, id, principal, perms });
883
+ },
884
+ async revoke(id, principal) {
885
+ const r = await readOrThrow(id);
886
+ const next = { ...r.accessRules };
887
+ delete next[principal];
888
+ await store.setAccess(id, next);
889
+ if (inspectorEnabled) emitInspectorEvent({ type: "store.revoke", store: name, id, principal });
890
+ },
891
+ async delete(id) {
892
+ await store.delete(id);
893
+ },
894
+ list() {
895
+ return Promise.resolve(store.list());
896
+ }
897
+ };
898
+ }
783
899
  const api = {
784
900
  nodeId: instanceId,
785
901
  nodeName,
@@ -861,6 +977,11 @@ function createSuperLineServer(contract, opts) {
861
977
  }
862
978
  };
863
979
  },
980
+ store(name) {
981
+ const handle = storeApi[name];
982
+ if (!handle) throw new SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
983
+ return handle;
984
+ },
864
985
  async close() {
865
986
  if (closing) return;
866
987
  closing = true;
@@ -869,21 +990,15 @@ function createSuperLineServer(contract, opts) {
869
990
  for (const conn of inspectorConns) conn.close();
870
991
  await adapter.presence?.clearNode(instanceId);
871
992
  await adapter.close?.();
872
- await new Promise((resolve) => {
873
- wss.close(() => resolve());
874
- });
993
+ for (const transport of opts.transports) await transport.stop();
875
994
  }
876
995
  };
877
996
  return api;
878
997
  }
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
998
  export {
885
999
  Conn,
886
1000
  MemoryBus,
887
1001
  createInMemoryAdapter,
888
- createSuperLineServer
1002
+ createSuperLineServer,
1003
+ resolvePrincipal
889
1004
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@super-line/server",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.5.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-libp2p": "0.5.0",
72
+ "@super-line/adapter-zeromq": "0.5.0",
73
+ "@super-line/adapter-rabbitmq": "0.5.0",
74
+ "@super-line/transport-http": "0.5.0",
75
+ "@super-line/transport-loopback": "0.5.0",
76
+ "@super-line/adapter-redis": "0.5.0",
77
+ "@super-line/client": "0.5.0",
78
+ "@super-line/transport-websocket": "0.5.0"
75
79
  },
76
80
  "optionalDependencies": {
77
81
  "@standard-community/standard-json": "^0.3.5"