@super-line/server 0.1.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 +16870 -65
- package/dist/index.d.cts +157 -25
- package/dist/index.d.ts +157 -25
- package/dist/index.js +620 -59
- 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 +23 -4
package/dist/index.js
CHANGED
|
@@ -1,38 +1,71 @@
|
|
|
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, role, ctx, serializer) {
|
|
17
|
+
constructor(ws, id, role, ctx, serializer, backpressure, onEmit) {
|
|
13
18
|
this.ws = ws;
|
|
19
|
+
this.id = id;
|
|
14
20
|
this.role = role;
|
|
15
21
|
this.ctx = ctx;
|
|
16
22
|
this.serializer = serializer;
|
|
23
|
+
this.backpressure = backpressure;
|
|
24
|
+
this.onEmit = onEmit;
|
|
17
25
|
}
|
|
18
26
|
ws;
|
|
27
|
+
id;
|
|
19
28
|
role;
|
|
20
29
|
ctx;
|
|
21
30
|
serializer;
|
|
31
|
+
backpressure;
|
|
32
|
+
onEmit;
|
|
22
33
|
/** Namespaced channels (rooms + topics) this connection belongs to. */
|
|
23
34
|
channels = /* @__PURE__ */ new Set();
|
|
35
|
+
/** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
|
|
36
|
+
data = {};
|
|
37
|
+
/** When this connection was accepted (`Date.now()` at the upgrade). */
|
|
38
|
+
connectedAt = Date.now();
|
|
39
|
+
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
40
|
+
lastPingAt;
|
|
41
|
+
/** When a heartbeat pong was last received — liveness signal (managed by the server). */
|
|
42
|
+
lastPongAt;
|
|
43
|
+
/** Pings sent since the last pong; drives reaping (managed by the server). */
|
|
44
|
+
missedPongs = 0;
|
|
45
|
+
// true => the frame was handled by the backpressure policy and must not be sent
|
|
46
|
+
overBackpressure() {
|
|
47
|
+
const bp = this.backpressure;
|
|
48
|
+
if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
|
|
49
|
+
if (bp.onExceed === "drop") {
|
|
50
|
+
console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
this.ws.close(1013);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
24
56
|
/** Encode and send a frame (unicast, e.g. req/res). */
|
|
25
57
|
send(frame) {
|
|
26
|
-
if (this.ws.readyState !== this.ws.OPEN) return;
|
|
58
|
+
if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
|
|
27
59
|
this.ws.send(this.serializer.encode(frame));
|
|
28
60
|
}
|
|
29
61
|
/** Forward an already-encoded frame (fan-out path; encoded once at the source). */
|
|
30
62
|
sendRaw(payload) {
|
|
31
|
-
if (this.ws.readyState !== this.ws.OPEN) return;
|
|
63
|
+
if (this.ws.readyState !== this.ws.OPEN || this.overBackpressure()) return;
|
|
32
64
|
this.ws.send(payload);
|
|
33
65
|
}
|
|
34
66
|
/** Push an event to THIS connection (node-local). Scoped to the role's events. */
|
|
35
67
|
emit(event, data) {
|
|
68
|
+
this.onEmit?.(String(event), data);
|
|
36
69
|
this.send({ t: "evt", e: String(event), d: data });
|
|
37
70
|
}
|
|
38
71
|
/** Close the underlying socket. */
|
|
@@ -44,6 +77,8 @@ var Conn = class {
|
|
|
44
77
|
// src/memory-adapter.ts
|
|
45
78
|
var MemoryBus = class {
|
|
46
79
|
channels = /* @__PURE__ */ new Map();
|
|
80
|
+
// shared presence directory; in-memory liveness = "has a descriptor" (graceful del on disconnect)
|
|
81
|
+
descriptors = /* @__PURE__ */ new Map();
|
|
47
82
|
subscribe(channel, adapter) {
|
|
48
83
|
let set = this.channels.get(channel);
|
|
49
84
|
if (!set) {
|
|
@@ -64,12 +99,69 @@ var MemoryBus = class {
|
|
|
64
99
|
for (const adapter of set) adapter.deliver(channel, payload);
|
|
65
100
|
}
|
|
66
101
|
};
|
|
102
|
+
var MemoryPresence = class {
|
|
103
|
+
constructor(bus) {
|
|
104
|
+
this.bus = bus;
|
|
105
|
+
}
|
|
106
|
+
bus;
|
|
107
|
+
set(d) {
|
|
108
|
+
this.bus.descriptors.set(d.id, d);
|
|
109
|
+
}
|
|
110
|
+
del(connId) {
|
|
111
|
+
this.bus.descriptors.delete(connId);
|
|
112
|
+
}
|
|
113
|
+
beat() {
|
|
114
|
+
}
|
|
115
|
+
clearNode(nodeId) {
|
|
116
|
+
for (const [id, d] of this.bus.descriptors) if (d.nodeId === nodeId) this.bus.descriptors.delete(id);
|
|
117
|
+
}
|
|
118
|
+
addRoom(connId, room) {
|
|
119
|
+
const d = this.bus.descriptors.get(connId);
|
|
120
|
+
if (d && !d.rooms.includes(room)) d.rooms = [...d.rooms, room];
|
|
121
|
+
}
|
|
122
|
+
removeRoom(connId, room) {
|
|
123
|
+
const d = this.bus.descriptors.get(connId);
|
|
124
|
+
if (d) d.rooms = d.rooms.filter((r) => r !== room);
|
|
125
|
+
}
|
|
126
|
+
list() {
|
|
127
|
+
return [...this.bus.descriptors.values()];
|
|
128
|
+
}
|
|
129
|
+
get(connId) {
|
|
130
|
+
return this.bus.descriptors.get(connId);
|
|
131
|
+
}
|
|
132
|
+
byUser(userId) {
|
|
133
|
+
return this.list().filter((d) => d.userId === userId);
|
|
134
|
+
}
|
|
135
|
+
roomMembers(room) {
|
|
136
|
+
return this.list().filter((d) => d.rooms.includes(room));
|
|
137
|
+
}
|
|
138
|
+
count() {
|
|
139
|
+
return this.bus.descriptors.size;
|
|
140
|
+
}
|
|
141
|
+
topology() {
|
|
142
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
143
|
+
for (const d of this.list()) {
|
|
144
|
+
const set = byNode.get(d.nodeId);
|
|
145
|
+
if (set) set.push(d);
|
|
146
|
+
else byNode.set(d.nodeId, [d]);
|
|
147
|
+
}
|
|
148
|
+
return [...byNode.entries()].map(([nodeId, ds]) => ({
|
|
149
|
+
nodeId,
|
|
150
|
+
nodeName: ds[0]?.nodeName ?? nodeId,
|
|
151
|
+
connections: ds.length,
|
|
152
|
+
rooms: new Set(ds.flatMap((d) => d.rooms)).size,
|
|
153
|
+
alive: true
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
67
157
|
var MemoryAdapter = class {
|
|
68
158
|
constructor(bus) {
|
|
69
159
|
this.bus = bus;
|
|
160
|
+
this.presence = new MemoryPresence(bus);
|
|
70
161
|
}
|
|
71
162
|
bus;
|
|
72
163
|
handler;
|
|
164
|
+
presence;
|
|
73
165
|
subscribe(channel) {
|
|
74
166
|
this.bus.subscribe(channel, this);
|
|
75
167
|
}
|
|
@@ -93,93 +185,396 @@ function createInMemoryAdapter(bus = new MemoryBus()) {
|
|
|
93
185
|
// src/index.ts
|
|
94
186
|
var ROOM = "r:";
|
|
95
187
|
var TOPIC = "t:";
|
|
96
|
-
var
|
|
97
|
-
|
|
188
|
+
var CONN = "c:";
|
|
189
|
+
var USER = "u:";
|
|
190
|
+
var REPLY = "reply:";
|
|
191
|
+
var INSPECT = "i:";
|
|
192
|
+
function createSuperLineServer(contract, opts) {
|
|
98
193
|
const c = contract;
|
|
99
194
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
100
195
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
101
|
-
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
|
+
});
|
|
102
204
|
const conns = /* @__PURE__ */ new Set();
|
|
205
|
+
const inspectorConns = /* @__PURE__ */ new Set();
|
|
103
206
|
const members = /* @__PURE__ */ new Map();
|
|
104
|
-
const
|
|
207
|
+
const busListeners = /* @__PURE__ */ new Map();
|
|
105
208
|
const instanceId = randomUUID();
|
|
209
|
+
const nodeName = opts.nodeName ?? process.env.SUPER_LINE_NODE_NAME ?? instanceId.slice(0, 8);
|
|
210
|
+
const replyChannel = REPLY + instanceId;
|
|
106
211
|
let impl = {};
|
|
212
|
+
let closing = false;
|
|
213
|
+
let nextSReq = 1;
|
|
214
|
+
const originWaiters = /* @__PURE__ */ new Map();
|
|
215
|
+
const ownerRouting = /* @__PURE__ */ new Map();
|
|
216
|
+
function buildDescriptor(conn) {
|
|
217
|
+
const userId = opts.identify?.(conn);
|
|
218
|
+
const rooms = [];
|
|
219
|
+
for (const ch of conn.channels) if (ch.startsWith(ROOM)) rooms.push(ch.slice(ROOM.length));
|
|
220
|
+
return {
|
|
221
|
+
id: conn.id,
|
|
222
|
+
role: conn.role,
|
|
223
|
+
nodeId: instanceId,
|
|
224
|
+
nodeName,
|
|
225
|
+
connectedAt: conn.connectedAt,
|
|
226
|
+
...userId !== void 0 ? { userId } : {},
|
|
227
|
+
rooms,
|
|
228
|
+
...opts.describeConn?.(conn)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function presenceOrThrow() {
|
|
232
|
+
if (!adapter.presence) throw new Error("cluster queries require an adapter with presence support");
|
|
233
|
+
return adapter.presence;
|
|
234
|
+
}
|
|
107
235
|
adapter.onMessage((channel, payload) => {
|
|
108
|
-
if (channel ===
|
|
109
|
-
|
|
236
|
+
if (channel === replyChannel) {
|
|
237
|
+
handleReply(payload);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (channel.startsWith(CONN) || channel.startsWith(USER)) {
|
|
241
|
+
handlePersonal(channel, payload);
|
|
110
242
|
return;
|
|
111
243
|
}
|
|
112
244
|
const set = members.get(channel);
|
|
113
|
-
if (
|
|
114
|
-
|
|
245
|
+
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
246
|
+
const busSet = busListeners.get(channel);
|
|
247
|
+
if (busSet) deliverBus(payload, busSet);
|
|
115
248
|
});
|
|
116
|
-
|
|
117
|
-
function
|
|
118
|
-
|
|
249
|
+
void adapter.subscribe(replyChannel);
|
|
250
|
+
function handlePersonal(channel, payload) {
|
|
251
|
+
const set = members.get(channel);
|
|
252
|
+
if (!set) return;
|
|
253
|
+
let env;
|
|
119
254
|
try {
|
|
120
|
-
|
|
255
|
+
env = serializer.decode(payload);
|
|
121
256
|
} catch {
|
|
122
257
|
return;
|
|
123
258
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
259
|
+
if (env.p === "close") {
|
|
260
|
+
for (const conn of set) conn.close();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (env.p === "req") {
|
|
264
|
+
const localId = nextSReq++;
|
|
265
|
+
ownerRouting.set(localId, { origin: env.o, corrId: env.i, name: env.m });
|
|
266
|
+
for (const conn of set) conn.send({ t: "sreq", i: localId, m: env.m, d: env.d });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const frame = serializer.encode(env.f);
|
|
270
|
+
for (const conn of set) conn.sendRaw(frame);
|
|
271
|
+
}
|
|
272
|
+
function handleReply(payload) {
|
|
273
|
+
let env;
|
|
274
|
+
try {
|
|
275
|
+
env = serializer.decode(payload);
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const w = originWaiters.get(env.i);
|
|
280
|
+
if (!w) return;
|
|
281
|
+
originWaiters.delete(env.i);
|
|
282
|
+
if (w.timer) clearTimeout(w.timer);
|
|
283
|
+
if (env.ok) w.resolve(env.d);
|
|
284
|
+
else w.reject(new SuperLineError(env.code, env.m, env.d));
|
|
285
|
+
}
|
|
286
|
+
async function handleClientReply(conn, localId, result) {
|
|
287
|
+
const r = ownerRouting.get(localId);
|
|
288
|
+
if (!r) return;
|
|
289
|
+
ownerRouting.delete(localId);
|
|
290
|
+
let env;
|
|
291
|
+
if (result.ok) {
|
|
292
|
+
try {
|
|
293
|
+
const def = c.roles[conn.role]?.serverToClient?.[r.name] ?? c.shared?.serverToClient?.[r.name];
|
|
294
|
+
const out = def && "output" in def ? await validate(def.output, result.d) : result.d;
|
|
295
|
+
env = { i: r.corrId, ok: true, d: out };
|
|
296
|
+
} catch (err) {
|
|
297
|
+
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
298
|
+
env = { i: r.corrId, ok: false, code: e.code, m: e.message, d: e.data };
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
|
|
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
|
+
);
|
|
313
|
+
void adapter.publish(REPLY + r.origin, serializer.encode(env));
|
|
314
|
+
}
|
|
315
|
+
const hb = opts.heartbeat === false ? null : opts.heartbeat ?? {};
|
|
316
|
+
let hbTimer;
|
|
317
|
+
if (hb) {
|
|
318
|
+
hbTimer = setInterval(() => {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
void adapter.presence?.beat(instanceId);
|
|
321
|
+
for (const conn of conns) {
|
|
322
|
+
if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
|
|
323
|
+
conn.ws.terminate();
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
conn.missedPongs++;
|
|
327
|
+
conn.lastPingAt = now;
|
|
328
|
+
conn.ws.ping();
|
|
329
|
+
}
|
|
330
|
+
}, hb.interval ?? 3e4);
|
|
331
|
+
hbTimer.unref?.();
|
|
332
|
+
}
|
|
333
|
+
function emitInspectorEvent(event) {
|
|
334
|
+
if (!inspectorEnabled) return;
|
|
335
|
+
void adapter.publish(INSPECT + "events", serializer.encode({ t: "pub", c: "events", d: event }));
|
|
127
336
|
}
|
|
128
337
|
function joinChannel(conn, channel) {
|
|
129
338
|
conn.channels.add(channel);
|
|
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
|
+
}
|
|
130
346
|
const set = members.get(channel);
|
|
131
347
|
if (set) {
|
|
132
348
|
set.add(conn);
|
|
133
349
|
return;
|
|
134
350
|
}
|
|
351
|
+
const alreadySubscribed = busListeners.has(channel);
|
|
135
352
|
members.set(channel, /* @__PURE__ */ new Set([conn]));
|
|
136
|
-
return adapter.subscribe(channel);
|
|
353
|
+
if (!alreadySubscribed) return adapter.subscribe(channel);
|
|
137
354
|
}
|
|
138
355
|
function leaveChannel(conn, channel) {
|
|
139
356
|
const set = members.get(channel);
|
|
140
357
|
if (!set) return;
|
|
141
358
|
set.delete(conn);
|
|
142
359
|
conn.channels.delete(channel);
|
|
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
|
+
}
|
|
143
367
|
if (set.size === 0) {
|
|
144
368
|
members.delete(channel);
|
|
145
|
-
void adapter.unsubscribe(channel);
|
|
369
|
+
if (!busListeners.has(channel)) void adapter.unsubscribe(channel);
|
|
146
370
|
}
|
|
147
371
|
}
|
|
372
|
+
function isTopic(name, block) {
|
|
373
|
+
const def = block?.serverToClient?.[name];
|
|
374
|
+
return !!def && typeof def === "object" && "subscribe" in def && def.subscribe === true;
|
|
375
|
+
}
|
|
148
376
|
function topicNamespace(role, name) {
|
|
149
|
-
if (c.roles[role]
|
|
150
|
-
if (c.shared
|
|
377
|
+
if (isTopic(name, c.roles[role])) return role;
|
|
378
|
+
if (isTopic(name, c.shared)) return "shared";
|
|
151
379
|
return void 0;
|
|
152
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
|
+
};
|
|
153
496
|
if (opts.server) {
|
|
154
497
|
opts.server.on("upgrade", (req, socket, head) => {
|
|
155
498
|
void handleUpgrade(req, socket, head);
|
|
156
499
|
});
|
|
157
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
|
+
}
|
|
158
508
|
async function handleUpgrade(req, socket, head) {
|
|
159
509
|
if (opts.path) {
|
|
160
510
|
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
161
511
|
if (pathname !== opts.path) return;
|
|
162
512
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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;
|
|
170
530
|
}
|
|
171
531
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
172
|
-
const
|
|
173
|
-
|
|
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
|
+
);
|
|
174
542
|
ws.on("message", (data, isBinary) => {
|
|
175
543
|
void onMessage(conn, data, isBinary);
|
|
176
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
|
+
});
|
|
177
558
|
ws.on("close", (code) => {
|
|
178
559
|
conns.delete(conn);
|
|
179
560
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
180
|
-
|
|
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);
|
|
181
570
|
});
|
|
182
|
-
opts.onConnection?.(conn,
|
|
571
|
+
opts.onConnection?.(conn, ctx);
|
|
572
|
+
const descriptor = buildDescriptor(conn);
|
|
573
|
+
void adapter.presence?.set(descriptor);
|
|
574
|
+
emitInspectorEvent({ type: "connect", descriptor });
|
|
575
|
+
void joinChannel(conn, CONN + conn.id);
|
|
576
|
+
const uid = opts.identify?.(conn);
|
|
577
|
+
if (uid !== void 0) void joinChannel(conn, USER + uid);
|
|
183
578
|
});
|
|
184
579
|
}
|
|
185
580
|
async function onMessage(conn, data, isBinary) {
|
|
@@ -189,11 +584,19 @@ function createSocketServer(contract, opts) {
|
|
|
189
584
|
} catch {
|
|
190
585
|
return;
|
|
191
586
|
}
|
|
587
|
+
if (inspectorConns.has(conn)) {
|
|
588
|
+
await onInspectorFrame(conn, frame);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
192
591
|
if (frame.t === "req") await handleReq(conn, frame);
|
|
193
592
|
else if (frame.t === "sub") await handleSub(conn, frame);
|
|
194
593
|
else if (frame.t === "unsub") {
|
|
195
594
|
const ns = topicNamespace(conn.role, frame.c);
|
|
196
595
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
596
|
+
} else if (frame.t === "sres") {
|
|
597
|
+
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
598
|
+
} else if (frame.t === "serr") {
|
|
599
|
+
await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
|
|
197
600
|
}
|
|
198
601
|
}
|
|
199
602
|
function runMiddleware(info, terminal) {
|
|
@@ -217,10 +620,16 @@ function createSocketServer(contract, opts) {
|
|
|
217
620
|
});
|
|
218
621
|
} catch (err) {
|
|
219
622
|
opts.onError?.(err, info);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
});
|
|
224
633
|
}
|
|
225
634
|
}
|
|
226
635
|
async function handleReq(conn, frame) {
|
|
@@ -232,7 +641,23 @@ function createSocketServer(contract, opts) {
|
|
|
232
641
|
}
|
|
233
642
|
await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
|
|
234
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
|
+
});
|
|
235
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
|
+
});
|
|
236
661
|
conn.send({ t: "res", i: frame.i, d: output });
|
|
237
662
|
});
|
|
238
663
|
}
|
|
@@ -245,7 +670,7 @@ function createSocketServer(contract, opts) {
|
|
|
245
670
|
await dispatchOp(conn, frame.i, { kind: "subscribe", name: frame.c, conn }, async () => {
|
|
246
671
|
if (opts.authorizeSubscribe) {
|
|
247
672
|
const ok = await opts.authorizeSubscribe(frame.c, conn.ctx, conn);
|
|
248
|
-
if (ok === false) throw new
|
|
673
|
+
if (ok === false) throw new SuperLineError("FORBIDDEN", `Subscribe denied: ${frame.c}`);
|
|
249
674
|
}
|
|
250
675
|
await joinChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
251
676
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
@@ -261,17 +686,146 @@ function createSocketServer(contract, opts) {
|
|
|
261
686
|
leaveChannel(conn, channel);
|
|
262
687
|
},
|
|
263
688
|
broadcast(event, data) {
|
|
689
|
+
if (inspectorEnabled)
|
|
690
|
+
emitInspectorEvent({ type: "msg.broadcast", room: name, name: String(event), data: safeSnapshot(data) });
|
|
264
691
|
void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
|
|
265
692
|
},
|
|
266
693
|
get size() {
|
|
267
694
|
return members.get(channel)?.size ?? 0;
|
|
695
|
+
},
|
|
696
|
+
get connections() {
|
|
697
|
+
return [...members.get(channel) ?? []];
|
|
268
698
|
}
|
|
269
699
|
};
|
|
270
700
|
}
|
|
271
701
|
function publishTo(ns, name, data) {
|
|
272
|
-
|
|
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
|
+
})();
|
|
739
|
+
}
|
|
740
|
+
function personalTarget(channel) {
|
|
741
|
+
return {
|
|
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
|
+
});
|
|
750
|
+
const env = { p: "emit", f: { t: "evt", e: event, d: data } };
|
|
751
|
+
void adapter.publish(channel, serializer.encode(env));
|
|
752
|
+
},
|
|
753
|
+
close() {
|
|
754
|
+
void adapter.publish(channel, serializer.encode({ p: "close" }));
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function requestConn(id, name, input, opts2) {
|
|
759
|
+
const reqId = nextSReq++;
|
|
760
|
+
return new Promise((resolve, reject) => {
|
|
761
|
+
const ms = opts2?.timeout ?? 3e4;
|
|
762
|
+
const timer = ms > 0 ? setTimeout(() => {
|
|
763
|
+
originWaiters.delete(reqId);
|
|
764
|
+
reject(new SuperLineError("TIMEOUT", `Request '${name}' timed out`));
|
|
765
|
+
}, ms) : void 0;
|
|
766
|
+
originWaiters.set(reqId, { resolve, reject, timer });
|
|
767
|
+
opts2?.signal?.addEventListener(
|
|
768
|
+
"abort",
|
|
769
|
+
() => {
|
|
770
|
+
if (originWaiters.delete(reqId)) {
|
|
771
|
+
if (timer) clearTimeout(timer);
|
|
772
|
+
reject(new SuperLineError("BAD_REQUEST", "Aborted"));
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{ once: true }
|
|
776
|
+
);
|
|
777
|
+
if (inspectorEnabled)
|
|
778
|
+
emitInspectorEvent({ type: "msg.serverRequest", target: id, name, input: safeSnapshot(input) });
|
|
779
|
+
const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
|
|
780
|
+
void adapter.publish(CONN + id, serializer.encode(env));
|
|
781
|
+
});
|
|
273
782
|
}
|
|
274
783
|
const api = {
|
|
784
|
+
nodeId: instanceId,
|
|
785
|
+
nodeName,
|
|
786
|
+
get local() {
|
|
787
|
+
return {
|
|
788
|
+
connections: [...conns],
|
|
789
|
+
get rooms() {
|
|
790
|
+
return localRooms();
|
|
791
|
+
},
|
|
792
|
+
get topics() {
|
|
793
|
+
return localTopics();
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
cluster: {
|
|
798
|
+
async connections() {
|
|
799
|
+
return presenceOrThrow().list();
|
|
800
|
+
},
|
|
801
|
+
async count() {
|
|
802
|
+
return presenceOrThrow().count();
|
|
803
|
+
},
|
|
804
|
+
async byUser(userId) {
|
|
805
|
+
return presenceOrThrow().byUser(userId);
|
|
806
|
+
},
|
|
807
|
+
async room(name) {
|
|
808
|
+
return presenceOrThrow().roomMembers(name);
|
|
809
|
+
},
|
|
810
|
+
async topology() {
|
|
811
|
+
return presenceOrThrow().topology();
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
async isOnline(userId) {
|
|
815
|
+
return (await presenceOrThrow().byUser(userId)).length > 0;
|
|
816
|
+
},
|
|
817
|
+
toConn(id) {
|
|
818
|
+
const t = personalTarget(CONN + id);
|
|
819
|
+
return {
|
|
820
|
+
emit: t.emit,
|
|
821
|
+
close: t.close,
|
|
822
|
+
request: (name, input, opts2) => requestConn(id, String(name), input, opts2)
|
|
823
|
+
};
|
|
824
|
+
},
|
|
825
|
+
toUser(userId) {
|
|
826
|
+
const t = personalTarget(USER + userId);
|
|
827
|
+
return { emit: t.emit, disconnect: t.close };
|
|
828
|
+
},
|
|
275
829
|
implement(handlers) {
|
|
276
830
|
impl = handlers;
|
|
277
831
|
return api;
|
|
@@ -280,33 +834,40 @@ function createSocketServer(contract, opts) {
|
|
|
280
834
|
publish(topic, data) {
|
|
281
835
|
publishTo("shared", String(topic), data);
|
|
282
836
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
publishTo(role, String(topic), data);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
},
|
|
290
|
-
emitServer(event, data) {
|
|
291
|
-
void adapter.publish(S2S, serializer.encode({ from: instanceId, e: String(event), d: data }));
|
|
292
|
-
},
|
|
293
|
-
onServer(event, handler) {
|
|
294
|
-
const name = String(event);
|
|
295
|
-
let set = serverListeners.get(name);
|
|
837
|
+
subscribe(topic, handler) {
|
|
838
|
+
const channel = TOPIC + "shared:" + String(topic);
|
|
839
|
+
let set = busListeners.get(channel);
|
|
296
840
|
if (!set) {
|
|
297
841
|
set = /* @__PURE__ */ new Set();
|
|
298
|
-
|
|
842
|
+
busListeners.set(channel, set);
|
|
843
|
+
if (!members.has(channel)) void adapter.subscribe(channel);
|
|
299
844
|
}
|
|
300
|
-
|
|
845
|
+
const cb = handler;
|
|
846
|
+
set.add(cb);
|
|
301
847
|
return () => {
|
|
302
|
-
const current =
|
|
848
|
+
const current = busListeners.get(channel);
|
|
303
849
|
if (!current) return;
|
|
304
|
-
current.delete(
|
|
305
|
-
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
|
+
}
|
|
306
862
|
};
|
|
307
863
|
},
|
|
308
864
|
async close() {
|
|
865
|
+
if (closing) return;
|
|
866
|
+
closing = true;
|
|
867
|
+
if (hbTimer) clearInterval(hbTimer);
|
|
309
868
|
for (const conn of conns) conn.close();
|
|
869
|
+
for (const conn of inspectorConns) conn.close();
|
|
870
|
+
await adapter.presence?.clearNode(instanceId);
|
|
310
871
|
await adapter.close?.();
|
|
311
872
|
await new Promise((resolve) => {
|
|
312
873
|
wss.close(() => resolve());
|
|
@@ -324,5 +885,5 @@ export {
|
|
|
324
885
|
Conn,
|
|
325
886
|
MemoryBus,
|
|
326
887
|
createInMemoryAdapter,
|
|
327
|
-
|
|
888
|
+
createSuperLineServer
|
|
328
889
|
};
|