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