@super-line/server 0.2.0 → 0.3.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 +2 -2
- 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 +16589 -86
- package/dist/index.d.cts +50 -23
- package/dist/index.d.ts +50 -23
- package/dist/index.js +338 -79
- 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 +22 -4
package/dist/index.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
2
|
+
|
|
1
3
|
// src/index.ts
|
|
2
4
|
import { randomUUID } from "crypto";
|
|
3
5
|
import { WebSocketServer } from "ws";
|
|
4
6
|
import {
|
|
5
7
|
jsonSerializer,
|
|
6
8
|
validate,
|
|
7
|
-
|
|
9
|
+
SuperLineError,
|
|
10
|
+
INSPECTOR_SUBPROTOCOL,
|
|
11
|
+
INSPECTOR_ROLE,
|
|
12
|
+
classifyContract
|
|
8
13
|
} from "@super-line/core";
|
|
9
14
|
|
|
10
15
|
// src/conn.ts
|
|
11
16
|
var Conn = class {
|
|
12
|
-
constructor(ws, id, role, ctx, serializer, backpressure) {
|
|
17
|
+
constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
|
|
13
18
|
this.ws = ws;
|
|
14
19
|
this.id = id;
|
|
15
20
|
this.role = role;
|
|
16
21
|
this.ctx = ctx;
|
|
17
22
|
this.serializer = serializer;
|
|
18
23
|
this.backpressure = backpressure;
|
|
24
|
+
this.onEmit = onEmit;
|
|
19
25
|
}
|
|
20
26
|
ws;
|
|
21
27
|
id;
|
|
@@ -23,6 +29,7 @@ var Conn = class {
|
|
|
23
29
|
ctx;
|
|
24
30
|
serializer;
|
|
25
31
|
backpressure;
|
|
32
|
+
onEmit;
|
|
26
33
|
/** Namespaced channels (rooms + topics) this connection belongs to. */
|
|
27
34
|
channels = /* @__PURE__ */ new Set();
|
|
28
35
|
/** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
|
|
@@ -58,6 +65,7 @@ var Conn = class {
|
|
|
58
65
|
}
|
|
59
66
|
/** Push an event to THIS connection (node-local). Scoped to the role's events. */
|
|
60
67
|
emit(event, data) {
|
|
68
|
+
this.onEmit?.(String(event), data);
|
|
61
69
|
this.send({ t: "evt", e: String(event), d: data });
|
|
62
70
|
}
|
|
63
71
|
/** Close the underlying socket. */
|
|
@@ -139,6 +147,7 @@ var MemoryPresence = class {
|
|
|
139
147
|
}
|
|
140
148
|
return [...byNode.entries()].map(([nodeId, ds]) => ({
|
|
141
149
|
nodeId,
|
|
150
|
+
nodeName: ds[0]?.nodeName ?? nodeId,
|
|
142
151
|
connections: ds.length,
|
|
143
152
|
rooms: new Set(ds.flatMap((d) => d.rooms)).size,
|
|
144
153
|
alive: true
|
|
@@ -179,16 +188,25 @@ var TOPIC = "t:";
|
|
|
179
188
|
var CONN = "c:";
|
|
180
189
|
var USER = "u:";
|
|
181
190
|
var REPLY = "reply:";
|
|
182
|
-
var
|
|
183
|
-
function
|
|
191
|
+
var INSPECT = "i:";
|
|
192
|
+
function createSuperLineServer(contract, opts) {
|
|
184
193
|
const c = contract;
|
|
185
194
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
186
195
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
187
|
-
const
|
|
196
|
+
const inspectorEnabled = !!opts.inspector;
|
|
197
|
+
const inspectorRedact = new Set(
|
|
198
|
+
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
199
|
+
);
|
|
200
|
+
const wss = new WebSocketServer({
|
|
201
|
+
noServer: true,
|
|
202
|
+
handleProtocols: (protocols) => inspectorEnabled && protocols.has(INSPECTOR_SUBPROTOCOL) ? INSPECTOR_SUBPROTOCOL : false
|
|
203
|
+
});
|
|
188
204
|
const conns = /* @__PURE__ */ new Set();
|
|
205
|
+
const inspectorConns = /* @__PURE__ */ new Set();
|
|
189
206
|
const members = /* @__PURE__ */ new Map();
|
|
190
|
-
const
|
|
207
|
+
const busListeners = /* @__PURE__ */ new Map();
|
|
191
208
|
const instanceId = randomUUID();
|
|
209
|
+
const nodeName = opts.nodeName ?? process.env.SUPER_LINE_NODE_NAME ?? instanceId.slice(0, 8);
|
|
192
210
|
const replyChannel = REPLY + instanceId;
|
|
193
211
|
let impl = {};
|
|
194
212
|
let closing = false;
|
|
@@ -203,6 +221,7 @@ function createSocketServer(contract, opts) {
|
|
|
203
221
|
id: conn.id,
|
|
204
222
|
role: conn.role,
|
|
205
223
|
nodeId: instanceId,
|
|
224
|
+
nodeName,
|
|
206
225
|
connectedAt: conn.connectedAt,
|
|
207
226
|
...userId !== void 0 ? { userId } : {},
|
|
208
227
|
rooms,
|
|
@@ -214,10 +233,6 @@ function createSocketServer(contract, opts) {
|
|
|
214
233
|
return adapter.presence;
|
|
215
234
|
}
|
|
216
235
|
adapter.onMessage((channel, payload) => {
|
|
217
|
-
if (channel === S2S) {
|
|
218
|
-
handleServerMessage(payload);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
236
|
if (channel === replyChannel) {
|
|
222
237
|
handleReply(payload);
|
|
223
238
|
return;
|
|
@@ -227,8 +242,9 @@ function createSocketServer(contract, opts) {
|
|
|
227
242
|
return;
|
|
228
243
|
}
|
|
229
244
|
const set = members.get(channel);
|
|
230
|
-
if (
|
|
231
|
-
|
|
245
|
+
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
246
|
+
const busSet = busListeners.get(channel);
|
|
247
|
+
if (busSet) deliverBus(payload, busSet);
|
|
232
248
|
});
|
|
233
249
|
void adapter.subscribe(replyChannel);
|
|
234
250
|
function handlePersonal(channel, payload) {
|
|
@@ -265,7 +281,7 @@ function createSocketServer(contract, opts) {
|
|
|
265
281
|
originWaiters.delete(env.i);
|
|
266
282
|
if (w.timer) clearTimeout(w.timer);
|
|
267
283
|
if (env.ok) w.resolve(env.d);
|
|
268
|
-
else w.reject(new
|
|
284
|
+
else w.reject(new SuperLineError(env.code, env.m, env.d));
|
|
269
285
|
}
|
|
270
286
|
async function handleClientReply(conn, localId, result) {
|
|
271
287
|
const r = ownerRouting.get(localId);
|
|
@@ -278,15 +294,24 @@ function createSocketServer(contract, opts) {
|
|
|
278
294
|
const out = def && "output" in def ? await validate(def.output, result.d) : result.d;
|
|
279
295
|
env = { i: r.corrId, ok: true, d: out };
|
|
280
296
|
} catch (err) {
|
|
281
|
-
const e = err instanceof
|
|
297
|
+
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
282
298
|
env = { i: r.corrId, ok: false, code: e.code, m: e.message, d: e.data };
|
|
283
299
|
}
|
|
284
300
|
} else {
|
|
285
301
|
env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
|
|
286
302
|
}
|
|
303
|
+
if (inspectorEnabled)
|
|
304
|
+
emitInspectorEvent(
|
|
305
|
+
env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: safeSnapshot(env.d) } : {
|
|
306
|
+
type: "msg.serverReply",
|
|
307
|
+
target: conn.id,
|
|
308
|
+
name: r.name,
|
|
309
|
+
ok: false,
|
|
310
|
+
error: { code: env.code, message: env.m }
|
|
311
|
+
}
|
|
312
|
+
);
|
|
287
313
|
void adapter.publish(REPLY + r.origin, serializer.encode(env));
|
|
288
314
|
}
|
|
289
|
-
if (c.serverToServer) void adapter.subscribe(S2S);
|
|
290
315
|
const hb = opts.heartbeat === false ? null : opts.heartbeat ?? {};
|
|
291
316
|
let hbTimer;
|
|
292
317
|
if (hb) {
|
|
@@ -305,37 +330,43 @@ function createSocketServer(contract, opts) {
|
|
|
305
330
|
}, hb.interval ?? 3e4);
|
|
306
331
|
hbTimer.unref?.();
|
|
307
332
|
}
|
|
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);
|
|
333
|
+
function emitInspectorEvent(event) {
|
|
334
|
+
if (!inspectorEnabled) return;
|
|
335
|
+
void adapter.publish(INSPECT + "events", serializer.encode({ t: "pub", c: "events", d: event }));
|
|
318
336
|
}
|
|
319
337
|
function joinChannel(conn, channel) {
|
|
320
338
|
conn.channels.add(channel);
|
|
321
|
-
if (channel.startsWith(ROOM))
|
|
339
|
+
if (channel.startsWith(ROOM)) {
|
|
340
|
+
const room2 = channel.slice(ROOM.length);
|
|
341
|
+
void adapter.presence?.addRoom(conn.id, room2);
|
|
342
|
+
emitInspectorEvent({ type: "room.add", connId: conn.id, room: room2 });
|
|
343
|
+
} else if (channel.startsWith(TOPIC)) {
|
|
344
|
+
emitInspectorEvent({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
|
|
345
|
+
}
|
|
322
346
|
const set = members.get(channel);
|
|
323
347
|
if (set) {
|
|
324
348
|
set.add(conn);
|
|
325
349
|
return;
|
|
326
350
|
}
|
|
351
|
+
const alreadySubscribed = busListeners.has(channel);
|
|
327
352
|
members.set(channel, /* @__PURE__ */ new Set([conn]));
|
|
328
|
-
return adapter.subscribe(channel);
|
|
353
|
+
if (!alreadySubscribed) return adapter.subscribe(channel);
|
|
329
354
|
}
|
|
330
355
|
function leaveChannel(conn, channel) {
|
|
331
356
|
const set = members.get(channel);
|
|
332
357
|
if (!set) return;
|
|
333
358
|
set.delete(conn);
|
|
334
359
|
conn.channels.delete(channel);
|
|
335
|
-
if (channel.startsWith(ROOM))
|
|
360
|
+
if (channel.startsWith(ROOM)) {
|
|
361
|
+
const room2 = channel.slice(ROOM.length);
|
|
362
|
+
void adapter.presence?.removeRoom(conn.id, room2);
|
|
363
|
+
emitInspectorEvent({ type: "room.remove", connId: conn.id, room: room2 });
|
|
364
|
+
} else if (channel.startsWith(TOPIC)) {
|
|
365
|
+
emitInspectorEvent({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
|
|
366
|
+
}
|
|
336
367
|
if (set.size === 0) {
|
|
337
368
|
members.delete(channel);
|
|
338
|
-
void adapter.unsubscribe(channel);
|
|
369
|
+
if (!busListeners.has(channel)) void adapter.unsubscribe(channel);
|
|
339
370
|
}
|
|
340
371
|
}
|
|
341
372
|
function isTopic(name, block) {
|
|
@@ -347,42 +378,200 @@ function createSocketServer(contract, opts) {
|
|
|
347
378
|
if (isTopic(name, c.shared)) return "shared";
|
|
348
379
|
return void 0;
|
|
349
380
|
}
|
|
381
|
+
function localRooms() {
|
|
382
|
+
const out = [];
|
|
383
|
+
for (const channel of members.keys()) if (channel.startsWith(ROOM)) out.push(channel.slice(ROOM.length));
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
function localTopics() {
|
|
387
|
+
const out = [];
|
|
388
|
+
for (const channel of members.keys()) {
|
|
389
|
+
if (!channel.startsWith(TOPIC)) continue;
|
|
390
|
+
out.push(channel.slice(channel.indexOf(":", TOPIC.length) + 1));
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
async function onInspectorFrame(conn, frame) {
|
|
395
|
+
if (frame.t === "req") await handleInspectorReq(conn, frame);
|
|
396
|
+
else if (frame.t === "sub") await handleInspectorSub(conn, frame);
|
|
397
|
+
else if (frame.t === "unsub") leaveChannel(conn, INSPECT + "events");
|
|
398
|
+
}
|
|
399
|
+
async function handleInspectorSub(conn, frame) {
|
|
400
|
+
if (frame.c !== "events") {
|
|
401
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
await joinChannel(conn, INSPECT + "events");
|
|
405
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
406
|
+
}
|
|
407
|
+
async function handleInspectorReq(conn, frame) {
|
|
408
|
+
const handler = inspectorHandlers[frame.m];
|
|
409
|
+
if (!handler) {
|
|
410
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const output = await handler(frame.d, conn);
|
|
415
|
+
conn.send({ t: "res", i: frame.i, d: output });
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
418
|
+
conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function buildInspectedContract() {
|
|
422
|
+
let toJsonSchema;
|
|
423
|
+
try {
|
|
424
|
+
const mod = await import("./dist-S566F7HE.js");
|
|
425
|
+
toJsonSchema = mod.toJsonSchema;
|
|
426
|
+
} catch {
|
|
427
|
+
return classifyContract(c);
|
|
428
|
+
}
|
|
429
|
+
const schemas = /* @__PURE__ */ new Set();
|
|
430
|
+
classifyContract(c, (s) => {
|
|
431
|
+
schemas.add(s);
|
|
432
|
+
return void 0;
|
|
433
|
+
});
|
|
434
|
+
const converted = /* @__PURE__ */ new Map();
|
|
435
|
+
await Promise.all(
|
|
436
|
+
[...schemas].map(
|
|
437
|
+
(s) => toJsonSchema(s).then(
|
|
438
|
+
(j) => {
|
|
439
|
+
converted.set(s, j);
|
|
440
|
+
},
|
|
441
|
+
() => {
|
|
442
|
+
}
|
|
443
|
+
// unsupported vendor / missing per-vendor converter -> structure-only for this entry
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
return classifyContract(c, (s) => converted.get(s));
|
|
448
|
+
}
|
|
449
|
+
function safeSnapshot(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
450
|
+
if (value === null) return null;
|
|
451
|
+
const t = typeof value;
|
|
452
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
453
|
+
if (t === "function") return "[Function]";
|
|
454
|
+
if (t === "symbol") return value.toString();
|
|
455
|
+
if (t !== "object") return value;
|
|
456
|
+
const obj = value;
|
|
457
|
+
if (obj instanceof Date) return obj.toISOString();
|
|
458
|
+
if (seen.has(obj)) return "[Circular]";
|
|
459
|
+
if (depth >= 6) return "[MaxDepth]";
|
|
460
|
+
seen.add(obj);
|
|
461
|
+
try {
|
|
462
|
+
if (Array.isArray(obj)) return obj.slice(0, 1e3).map((v) => safeSnapshot(v, depth + 1, seen));
|
|
463
|
+
const ctor = Object.getPrototypeOf(obj)?.constructor?.name;
|
|
464
|
+
const out = {};
|
|
465
|
+
if (ctor && ctor !== "Object") out["#type"] = ctor;
|
|
466
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
467
|
+
out[k] = inspectorRedact.has(k) ? "[Redacted]" : safeSnapshot(v, depth + 1, seen);
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
} finally {
|
|
471
|
+
seen.delete(obj);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const inspectorHandlers = {
|
|
475
|
+
getContract: () => buildInspectedContract(),
|
|
476
|
+
getTopology: async () => presenceOrThrow().topology(),
|
|
477
|
+
listConnections: async () => presenceOrThrow().list(),
|
|
478
|
+
getNode: async () => ({ nodeId: instanceId, nodeName, rooms: localRooms(), topics: localTopics() }),
|
|
479
|
+
getConn: async (input) => {
|
|
480
|
+
const id = input?.id;
|
|
481
|
+
if (!id) throw new SuperLineError("BAD_REQUEST", "getConn requires an id");
|
|
482
|
+
const local = [...conns].find((cn) => cn.id === id);
|
|
483
|
+
if (local) {
|
|
484
|
+
return {
|
|
485
|
+
descriptor: buildDescriptor(local),
|
|
486
|
+
ctx: safeSnapshot(local.ctx),
|
|
487
|
+
data: safeSnapshot(local.data),
|
|
488
|
+
ctxAvailable: true
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const remote = await presenceOrThrow().get(id);
|
|
492
|
+
if (!remote) throw new SuperLineError("NOT_FOUND", `Unknown connection: ${id}`);
|
|
493
|
+
return { descriptor: remote, ctxAvailable: false };
|
|
494
|
+
}
|
|
495
|
+
};
|
|
350
496
|
if (opts.server) {
|
|
351
497
|
opts.server.on("upgrade", (req, socket, head) => {
|
|
352
498
|
void handleUpgrade(req, socket, head);
|
|
353
499
|
});
|
|
354
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
|
+
}
|
|
355
508
|
async function handleUpgrade(req, socket, head) {
|
|
356
509
|
if (opts.path) {
|
|
357
510
|
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
358
511
|
if (pathname !== opts.path) return;
|
|
359
512
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
513
|
+
const inspector = isInspectorRequest(req);
|
|
514
|
+
let role;
|
|
515
|
+
let ctx;
|
|
516
|
+
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;
|
|
367
530
|
}
|
|
368
531
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
369
|
-
const
|
|
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
|
+
}
|
|
370
553
|
conns.add(conn);
|
|
371
554
|
ws.on("pong", () => {
|
|
372
555
|
conn.lastPongAt = Date.now();
|
|
373
556
|
conn.missedPongs = 0;
|
|
374
557
|
});
|
|
375
|
-
ws.on("message", (data, isBinary) => {
|
|
376
|
-
void onMessage(conn, data, isBinary);
|
|
377
|
-
});
|
|
378
558
|
ws.on("close", (code) => {
|
|
379
559
|
conns.delete(conn);
|
|
380
560
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
381
561
|
void adapter.presence?.del(conn.id);
|
|
382
|
-
opts.
|
|
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);
|
|
383
570
|
});
|
|
384
|
-
opts.onConnection?.(conn,
|
|
385
|
-
|
|
571
|
+
opts.onConnection?.(conn, ctx);
|
|
572
|
+
const descriptor = buildDescriptor(conn);
|
|
573
|
+
void adapter.presence?.set(descriptor);
|
|
574
|
+
emitInspectorEvent({ type: "connect", descriptor });
|
|
386
575
|
void joinChannel(conn, CONN + conn.id);
|
|
387
576
|
const uid = opts.identify?.(conn);
|
|
388
577
|
if (uid !== void 0) void joinChannel(conn, USER + uid);
|
|
@@ -395,6 +584,10 @@ function createSocketServer(contract, opts) {
|
|
|
395
584
|
} catch {
|
|
396
585
|
return;
|
|
397
586
|
}
|
|
587
|
+
if (inspectorConns.has(conn)) {
|
|
588
|
+
await onInspectorFrame(conn, frame);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
398
591
|
if (frame.t === "req") await handleReq(conn, frame);
|
|
399
592
|
else if (frame.t === "sub") await handleSub(conn, frame);
|
|
400
593
|
else if (frame.t === "unsub") {
|
|
@@ -427,10 +620,16 @@ function createSocketServer(contract, opts) {
|
|
|
427
620
|
});
|
|
428
621
|
} catch (err) {
|
|
429
622
|
opts.onError?.(err, info);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
623
|
+
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
624
|
+
if (!responded) conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
|
|
625
|
+
if (inspectorEnabled && info.kind === "request")
|
|
626
|
+
emitInspectorEvent({
|
|
627
|
+
type: "msg.response",
|
|
628
|
+
connId: conn.id,
|
|
629
|
+
name: info.name,
|
|
630
|
+
ok: false,
|
|
631
|
+
error: { code: e.code, message: e.message }
|
|
632
|
+
});
|
|
434
633
|
}
|
|
435
634
|
}
|
|
436
635
|
async function handleReq(conn, frame) {
|
|
@@ -442,7 +641,23 @@ function createSocketServer(contract, opts) {
|
|
|
442
641
|
}
|
|
443
642
|
await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
|
|
444
643
|
const input = await validate(def.input, frame.d);
|
|
644
|
+
if (inspectorEnabled)
|
|
645
|
+
emitInspectorEvent({
|
|
646
|
+
type: "msg.request",
|
|
647
|
+
connId: conn.id,
|
|
648
|
+
role: conn.role,
|
|
649
|
+
name: frame.m,
|
|
650
|
+
input: safeSnapshot(input)
|
|
651
|
+
});
|
|
445
652
|
const output = await handler(input, conn.ctx, conn);
|
|
653
|
+
if (inspectorEnabled)
|
|
654
|
+
emitInspectorEvent({
|
|
655
|
+
type: "msg.response",
|
|
656
|
+
connId: conn.id,
|
|
657
|
+
name: frame.m,
|
|
658
|
+
ok: true,
|
|
659
|
+
output: safeSnapshot(output)
|
|
660
|
+
});
|
|
446
661
|
conn.send({ t: "res", i: frame.i, d: output });
|
|
447
662
|
});
|
|
448
663
|
}
|
|
@@ -455,7 +670,7 @@ function createSocketServer(contract, opts) {
|
|
|
455
670
|
await dispatchOp(conn, frame.i, { kind: "subscribe", name: frame.c, conn }, async () => {
|
|
456
671
|
if (opts.authorizeSubscribe) {
|
|
457
672
|
const ok = await opts.authorizeSubscribe(frame.c, conn.ctx, conn);
|
|
458
|
-
if (ok === false) throw new
|
|
673
|
+
if (ok === false) throw new SuperLineError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
|
|
459
674
|
}
|
|
460
675
|
await joinChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
461
676
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
@@ -471,6 +686,8 @@ function createSocketServer(contract, opts) {
|
|
|
471
686
|
leaveChannel(conn, channel);
|
|
472
687
|
},
|
|
473
688
|
broadcast(event, data) {
|
|
689
|
+
if (inspectorEnabled)
|
|
690
|
+
emitInspectorEvent({ type: "msg.broadcast", room: name, name: String(event), data: safeSnapshot(data) });
|
|
474
691
|
void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
|
|
475
692
|
},
|
|
476
693
|
get size() {
|
|
@@ -482,11 +699,54 @@ function createSocketServer(contract, opts) {
|
|
|
482
699
|
};
|
|
483
700
|
}
|
|
484
701
|
function publishTo(ns, name, data) {
|
|
485
|
-
|
|
702
|
+
const channel = TOPIC + ns + ":" + name;
|
|
703
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "msg.publish", topic: name, data: safeSnapshot(data) });
|
|
704
|
+
const busSet = busListeners.get(channel);
|
|
705
|
+
if (busSet) for (const cb of busSet) callBus(cb, data, instanceId, name);
|
|
706
|
+
void adapter.publish(channel, serializer.encode({ t: "pub", c: name, d: data, i: instanceId }));
|
|
707
|
+
}
|
|
708
|
+
function callBus(cb, data, from, name) {
|
|
709
|
+
try {
|
|
710
|
+
cb(data, { from });
|
|
711
|
+
} catch (err) {
|
|
712
|
+
opts.onError?.(err, { kind: "event", name });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function deliverBus(payload, set) {
|
|
716
|
+
let frame;
|
|
717
|
+
try {
|
|
718
|
+
frame = serializer.decode(payload);
|
|
719
|
+
} catch {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (frame.i === instanceId) return;
|
|
723
|
+
const from = frame.i ?? "";
|
|
724
|
+
const name = frame.c;
|
|
725
|
+
const def = c.shared?.serverToClient?.[name];
|
|
726
|
+
const schema = def && typeof def === "object" && "payload" in def ? def.payload : void 0;
|
|
727
|
+
void (async () => {
|
|
728
|
+
let data = frame.d;
|
|
729
|
+
if (schema) {
|
|
730
|
+
try {
|
|
731
|
+
data = await validate(schema, frame.d);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
opts.onError?.(err, { kind: "event", name });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const cb of set) callBus(cb, data, from, name);
|
|
738
|
+
})();
|
|
486
739
|
}
|
|
487
740
|
function personalTarget(channel) {
|
|
488
741
|
return {
|
|
489
742
|
emit(event, data) {
|
|
743
|
+
if (inspectorEnabled)
|
|
744
|
+
emitInspectorEvent({
|
|
745
|
+
type: "msg.event",
|
|
746
|
+
target: channel.slice(channel.indexOf(":") + 1),
|
|
747
|
+
name: event,
|
|
748
|
+
data: safeSnapshot(data)
|
|
749
|
+
});
|
|
490
750
|
const env = { p: "emit", f: { t: "evt", e: event, d: data } };
|
|
491
751
|
void adapter.publish(channel, serializer.encode(env));
|
|
492
752
|
},
|
|
@@ -501,7 +761,7 @@ function createSocketServer(contract, opts) {
|
|
|
501
761
|
const ms = opts2?.timeout ?? 3e4;
|
|
502
762
|
const timer = ms > 0 ? setTimeout(() => {
|
|
503
763
|
originWaiters.delete(reqId);
|
|
504
|
-
reject(new
|
|
764
|
+
reject(new SuperLineError("TIMEOUT", `Request '${name}' timed out`));
|
|
505
765
|
}, ms) : void 0;
|
|
506
766
|
originWaiters.set(reqId, { resolve, reject, timer });
|
|
507
767
|
opts2?.signal?.addEventListener(
|
|
@@ -509,32 +769,28 @@ function createSocketServer(contract, opts) {
|
|
|
509
769
|
() => {
|
|
510
770
|
if (originWaiters.delete(reqId)) {
|
|
511
771
|
if (timer) clearTimeout(timer);
|
|
512
|
-
reject(new
|
|
772
|
+
reject(new SuperLineError("BAD_REQUEST", "Aborted"));
|
|
513
773
|
}
|
|
514
774
|
},
|
|
515
775
|
{ once: true }
|
|
516
776
|
);
|
|
777
|
+
if (inspectorEnabled)
|
|
778
|
+
emitInspectorEvent({ type: "msg.serverRequest", target: id, name, input: safeSnapshot(input) });
|
|
517
779
|
const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
|
|
518
780
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
519
781
|
});
|
|
520
782
|
}
|
|
521
783
|
const api = {
|
|
522
784
|
nodeId: instanceId,
|
|
785
|
+
nodeName,
|
|
523
786
|
get local() {
|
|
524
787
|
return {
|
|
525
788
|
connections: [...conns],
|
|
526
789
|
get rooms() {
|
|
527
|
-
|
|
528
|
-
for (const channel of members.keys()) if (channel.startsWith(ROOM)) out.push(channel.slice(ROOM.length));
|
|
529
|
-
return out;
|
|
790
|
+
return localRooms();
|
|
530
791
|
},
|
|
531
792
|
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;
|
|
793
|
+
return localTopics();
|
|
538
794
|
}
|
|
539
795
|
};
|
|
540
796
|
},
|
|
@@ -578,29 +834,31 @@ function createSocketServer(contract, opts) {
|
|
|
578
834
|
publish(topic, data) {
|
|
579
835
|
publishTo("shared", String(topic), data);
|
|
580
836
|
},
|
|
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);
|
|
837
|
+
subscribe(topic, handler) {
|
|
838
|
+
const channel = TOPIC + "shared:" + String(topic);
|
|
839
|
+
let set = busListeners.get(channel);
|
|
594
840
|
if (!set) {
|
|
595
841
|
set = /* @__PURE__ */ new Set();
|
|
596
|
-
|
|
842
|
+
busListeners.set(channel, set);
|
|
843
|
+
if (!members.has(channel)) void adapter.subscribe(channel);
|
|
597
844
|
}
|
|
598
|
-
|
|
845
|
+
const cb = handler;
|
|
846
|
+
set.add(cb);
|
|
599
847
|
return () => {
|
|
600
|
-
const current =
|
|
848
|
+
const current = busListeners.get(channel);
|
|
601
849
|
if (!current) return;
|
|
602
|
-
current.delete(
|
|
603
|
-
if (current.size === 0)
|
|
850
|
+
current.delete(cb);
|
|
851
|
+
if (current.size === 0) {
|
|
852
|
+
busListeners.delete(channel);
|
|
853
|
+
if (!members.has(channel)) void adapter.unsubscribe(channel);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
},
|
|
857
|
+
forRole(role) {
|
|
858
|
+
return {
|
|
859
|
+
publish(topic, data) {
|
|
860
|
+
publishTo(role, String(topic), data);
|
|
861
|
+
}
|
|
604
862
|
};
|
|
605
863
|
},
|
|
606
864
|
async close() {
|
|
@@ -608,6 +866,7 @@ function createSocketServer(contract, opts) {
|
|
|
608
866
|
closing = true;
|
|
609
867
|
if (hbTimer) clearInterval(hbTimer);
|
|
610
868
|
for (const conn of conns) conn.close();
|
|
869
|
+
for (const conn of inspectorConns) conn.close();
|
|
611
870
|
await adapter.presence?.clearNode(instanceId);
|
|
612
871
|
await adapter.close?.();
|
|
613
872
|
await new Promise((resolve) => {
|
|
@@ -626,5 +885,5 @@ export {
|
|
|
626
885
|
Conn,
|
|
627
886
|
MemoryBus,
|
|
628
887
|
createInMemoryAdapter,
|
|
629
|
-
|
|
888
|
+
createSuperLineServer
|
|
630
889
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MissingDependencyError
|
|
3
|
+
} from "./chunk-BXHVPS35.js";
|
|
4
|
+
import "./chunk-MLKGABMK.js";
|
|
5
|
+
|
|
6
|
+
// ../../node_modules/.pnpm/@standard-community+standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@_c5121037ae681f5d570421df77d4831b/node_modules/@standard-community/standard-json/dist/sury-CWZTCd75.js
|
|
7
|
+
async function getToJsonSchemaFn() {
|
|
8
|
+
try {
|
|
9
|
+
const { toJSONSchema } = await import("sury");
|
|
10
|
+
return toJSONSchema;
|
|
11
|
+
} catch {
|
|
12
|
+
throw new MissingDependencyError("sury");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
getToJsonSchemaFn as default
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// ../../node_modules/.pnpm/@standard-community+standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@_c5121037ae681f5d570421df77d4831b/node_modules/@standard-community/standard-json/dist/typebox-Dei93FPO.js
|
|
4
|
+
function getToJsonSchemaFn() {
|
|
5
|
+
return (schema) => JSON.parse(JSON.stringify(schema.Type()));
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
getToJsonSchemaFn as default
|
|
9
|
+
};
|