@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 +5 -4
- package/dist/index.cjs +78 -119
- package/dist/index.d.cts +23 -34
- package/dist/index.d.ts +23 -34
- package/dist/index.js +78 -120
- package/package.json +12 -8
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: (
|
|
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(
|
|
16285
|
-
this.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
16326
|
-
this.
|
|
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.
|
|
16331
|
-
this.
|
|
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
|
-
/**
|
|
16326
|
+
/** Graceful close of the underlying transport connection. */
|
|
16339
16327
|
close() {
|
|
16340
|
-
this.
|
|
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.
|
|
16579
|
+
conn.terminate();
|
|
16591
16580
|
continue;
|
|
16592
16581
|
}
|
|
16593
16582
|
conn.missedPongs++;
|
|
16594
16583
|
conn.lastPingAt = now;
|
|
16595
|
-
conn.
|
|
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
|
-
|
|
16764
|
-
opts.
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
const
|
|
16771
|
-
if (!
|
|
16772
|
-
|
|
16773
|
-
|
|
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
|
|
16781
|
-
|
|
16782
|
-
|
|
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
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
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
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
const
|
|
16844
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
22
|
-
readonly
|
|
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
|
-
/**
|
|
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
|
|
47
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
223
|
-
|
|
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
|
-
/**
|
|
236
|
-
authenticate: (
|
|
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
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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: (
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
22
|
-
readonly
|
|
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
|
-
/**
|
|
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
|
|
47
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
223
|
-
|
|
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
|
-
/**
|
|
236
|
-
authenticate: (
|
|
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
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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: (
|
|
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
|
|
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(
|
|
18
|
-
this.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
59
|
-
this.
|
|
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.
|
|
64
|
-
this.
|
|
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
|
-
/**
|
|
58
|
+
/** Graceful close of the underlying transport connection. */
|
|
72
59
|
close() {
|
|
73
|
-
this.
|
|
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.
|
|
311
|
+
conn.terminate();
|
|
324
312
|
continue;
|
|
325
313
|
}
|
|
326
314
|
conn.missedPongs++;
|
|
327
315
|
conn.lastPingAt = now;
|
|
328
|
-
conn.
|
|
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
|
-
|
|
497
|
-
opts.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
if (!
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
"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
|
-
"
|
|
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-
|
|
71
|
-
"@super-line/adapter-libp2p": "0.
|
|
72
|
-
"@super-line/
|
|
73
|
-
"@super-line/
|
|
74
|
-
"@super-line/
|
|
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"
|