@super-line/server 0.9.0 → 0.10.1
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 +1 -1
- package/dist/index.cjs +348 -16480
- package/dist/index.d.cts +175 -20
- package/dist/index.d.ts +175 -20
- package/dist/index.js +317 -205
- package/package.json +8 -7
- package/dist/arktype-aI7TBD0R-QUZZIMBF.js +0 -9
- package/dist/chunk-BXHVPS35.js +0 -164
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/core-DXJRFNRR.js +0 -9858
- package/dist/dist-S566F7HE.js +0 -9
- package/dist/effect-QlVUlMFu-XUZCEH2A.js +0 -17
- package/dist/esm-F5WYXMGY.js +0 -5222
- package/dist/sury-CWZTCd75-XRKNS4FU.js +0 -17
- package/dist/typebox-Dei93FPO-EURKZGC4.js +0 -9
- package/dist/valibot--1zFm7rT-CYY6WSKB.js +0 -17
- package/dist/zod-Bwrt9trS-3RR2G3Z5.js +0 -31
package/dist/index.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import "./chunk-MLKGABMK.js";
|
|
2
|
-
|
|
3
1
|
// src/index.ts
|
|
4
2
|
import {
|
|
5
3
|
jsonSerializer,
|
|
6
4
|
validate,
|
|
7
|
-
SuperLineError
|
|
8
|
-
INSPECTOR_ROLE,
|
|
9
|
-
classifyContract,
|
|
10
|
-
eventPayload
|
|
5
|
+
SuperLineError
|
|
11
6
|
} from "@super-line/core";
|
|
12
7
|
|
|
13
8
|
// src/conn.ts
|
|
@@ -185,26 +180,82 @@ var TOPIC = "t:";
|
|
|
185
180
|
var CONN = "c:";
|
|
186
181
|
var USER = "u:";
|
|
187
182
|
var REPLY = "reply:";
|
|
188
|
-
var
|
|
189
|
-
var inspectorEncoder = new TextEncoder();
|
|
190
|
-
function encodedByteSize(encoded) {
|
|
191
|
-
return typeof encoded === "string" ? inspectorEncoder.encode(encoded).length : encoded.byteLength;
|
|
192
|
-
}
|
|
183
|
+
var PLUGIN = "x:";
|
|
193
184
|
var STORE = "s:";
|
|
194
185
|
var SERVER_ORIGIN = "server";
|
|
195
186
|
function createSuperLineServer(contract, opts) {
|
|
196
187
|
const c = contract;
|
|
197
188
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
198
189
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
199
|
-
const inspectorEnabled = !!opts.inspector;
|
|
200
|
-
const inspectorRedact = new Set(
|
|
201
|
-
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
202
|
-
);
|
|
203
190
|
const storeMap = opts.stores ?? {};
|
|
191
|
+
const plugins = opts.plugins ?? [];
|
|
192
|
+
const pluginNames = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const p of plugins) {
|
|
194
|
+
if (pluginNames.has(p.name)) throw new Error(`Duplicate plugin name: ${p.name}`);
|
|
195
|
+
pluginNames.add(p.name);
|
|
196
|
+
}
|
|
197
|
+
for (const p of plugins) {
|
|
198
|
+
if (!p.stores) continue;
|
|
199
|
+
for (const [name, store] of Object.entries(p.stores)) {
|
|
200
|
+
if (name in storeMap)
|
|
201
|
+
throw new Error(`Plugin '${p.name}' store '${name}' collides with an existing store of that name`);
|
|
202
|
+
storeMap[name] = store;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const reserved = [];
|
|
206
|
+
for (const p of plugins) {
|
|
207
|
+
if (!p.connection) continue;
|
|
208
|
+
const { role, subprotocol, match } = p.connection;
|
|
209
|
+
if (reserved.some((r) => r.role === role) || role in c.roles)
|
|
210
|
+
throw new Error(`Plugin '${p.name}' reserved role '${role}' collides with an existing role`);
|
|
211
|
+
reserved.push({ role, subprotocol, match });
|
|
212
|
+
}
|
|
213
|
+
const reservedRoles = new Set(reserved.map((r) => r.role));
|
|
214
|
+
const reservedServing = /* @__PURE__ */ new Map();
|
|
215
|
+
const errorHandlers = [];
|
|
216
|
+
if (opts.onError) errorHandlers.push(opts.onError);
|
|
217
|
+
for (const p of plugins) if (p.onError) errorHandlers.push(p.onError);
|
|
218
|
+
function fireError(error, info) {
|
|
219
|
+
for (const handler of errorHandlers) {
|
|
220
|
+
try {
|
|
221
|
+
handler(error, info);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const connectionHooks = [];
|
|
227
|
+
if (opts.onConnection) connectionHooks.push(opts.onConnection);
|
|
228
|
+
for (const p of plugins) if (p.onConnection) connectionHooks.push(p.onConnection);
|
|
229
|
+
function fireConnection(conn, ctx) {
|
|
230
|
+
for (const handler of connectionHooks) {
|
|
231
|
+
try {
|
|
232
|
+
handler(conn, ctx);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
fireError(err, { kind: "connect", name: "onConnection", conn });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const disconnectHooks = [];
|
|
239
|
+
if (opts.onDisconnect) disconnectHooks.push(opts.onDisconnect);
|
|
240
|
+
for (const p of plugins) if (p.onDisconnect) disconnectHooks.push(p.onDisconnect);
|
|
241
|
+
function fireDisconnect(conn, ctx, code) {
|
|
242
|
+
for (const handler of disconnectHooks) {
|
|
243
|
+
try {
|
|
244
|
+
handler(conn, ctx, code);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
fireError(err, { kind: "disconnect", name: "onDisconnect", conn });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const middlewareChain = [
|
|
251
|
+
...opts.use ?? [],
|
|
252
|
+
...plugins.flatMap((p) => p.use ?? [])
|
|
253
|
+
];
|
|
204
254
|
const conns = /* @__PURE__ */ new Set();
|
|
205
|
-
const
|
|
255
|
+
const reservedConns = /* @__PURE__ */ new Set();
|
|
206
256
|
const members = /* @__PURE__ */ new Map();
|
|
207
257
|
const busListeners = /* @__PURE__ */ new Map();
|
|
258
|
+
const pluginChannels = /* @__PURE__ */ new Map();
|
|
208
259
|
const instanceId = randomUUID();
|
|
209
260
|
const envNodeName = typeof process !== "undefined" ? process.env.SUPER_LINE_NODE_NAME : void 0;
|
|
210
261
|
const nodeName = opts.nodeName ?? envNodeName ?? instanceId.slice(0, 8);
|
|
@@ -248,6 +299,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
248
299
|
handleStoreRelay(channel, payload);
|
|
249
300
|
return;
|
|
250
301
|
}
|
|
302
|
+
if (channel.startsWith(PLUGIN)) {
|
|
303
|
+
handlePluginChannel(channel, payload);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
251
306
|
const set = members.get(channel);
|
|
252
307
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
253
308
|
const busSet = busListeners.get(channel);
|
|
@@ -307,9 +362,9 @@ function createSuperLineServer(contract, opts) {
|
|
|
307
362
|
} else {
|
|
308
363
|
env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
|
|
309
364
|
}
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output:
|
|
365
|
+
if (taps.length)
|
|
366
|
+
emitTap(
|
|
367
|
+
env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: env.d, reqId: r.corrId } : {
|
|
313
368
|
type: "msg.serverReply",
|
|
314
369
|
target: conn.id,
|
|
315
370
|
name: r.name,
|
|
@@ -338,25 +393,26 @@ function createSuperLineServer(contract, opts) {
|
|
|
338
393
|
}, hb.interval ?? 3e4);
|
|
339
394
|
hbTimer.unref?.();
|
|
340
395
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
396
|
+
const taps = [];
|
|
397
|
+
function emitTap(event) {
|
|
398
|
+
if (!taps.length) return;
|
|
399
|
+
for (const tap of taps) {
|
|
400
|
+
try {
|
|
401
|
+
tap(event);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
fireError(err, { kind: "event", name: event.type });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
351
406
|
}
|
|
407
|
+
for (const p of plugins) if (p.onEvent) taps.push(p.onEvent);
|
|
352
408
|
function joinChannel(conn, channel) {
|
|
353
409
|
conn.channels.add(channel);
|
|
354
410
|
if (channel.startsWith(ROOM)) {
|
|
355
411
|
const room2 = channel.slice(ROOM.length);
|
|
356
412
|
void adapter.presence?.addRoom(conn.id, room2);
|
|
357
|
-
|
|
413
|
+
emitTap({ type: "room.add", connId: conn.id, room: room2 });
|
|
358
414
|
} else if (channel.startsWith(TOPIC)) {
|
|
359
|
-
|
|
415
|
+
emitTap({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
|
|
360
416
|
}
|
|
361
417
|
const set = members.get(channel);
|
|
362
418
|
if (set) {
|
|
@@ -375,9 +431,9 @@ function createSuperLineServer(contract, opts) {
|
|
|
375
431
|
if (channel.startsWith(ROOM)) {
|
|
376
432
|
const room2 = channel.slice(ROOM.length);
|
|
377
433
|
void adapter.presence?.removeRoom(conn.id, room2);
|
|
378
|
-
|
|
434
|
+
emitTap({ type: "room.remove", connId: conn.id, room: room2 });
|
|
379
435
|
} else if (channel.startsWith(TOPIC)) {
|
|
380
|
-
|
|
436
|
+
emitTap({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
|
|
381
437
|
}
|
|
382
438
|
if (set.size === 0) {
|
|
383
439
|
members.delete(channel);
|
|
@@ -406,128 +462,47 @@ function createSuperLineServer(contract, opts) {
|
|
|
406
462
|
}
|
|
407
463
|
return out;
|
|
408
464
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
await joinChannel(conn, INSPECT + "events");
|
|
420
|
-
conn.send({ t: "res", i: frame.i, d: null });
|
|
421
|
-
}
|
|
422
|
-
async function handleInspectorReq(conn, frame) {
|
|
423
|
-
const handler = inspectorHandlers[frame.m];
|
|
424
|
-
if (!handler) {
|
|
425
|
-
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
try {
|
|
429
|
-
const output = await handler(frame.d, conn);
|
|
430
|
-
conn.send({ t: "res", i: frame.i, d: output });
|
|
431
|
-
} catch (err) {
|
|
432
|
-
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
433
|
-
conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
async function buildInspectedContract() {
|
|
437
|
-
let toJsonSchema;
|
|
438
|
-
try {
|
|
439
|
-
const mod = await import("./dist-S566F7HE.js");
|
|
440
|
-
toJsonSchema = mod.toJsonSchema;
|
|
441
|
-
} catch {
|
|
442
|
-
return classifyContract(c);
|
|
443
|
-
}
|
|
444
|
-
const schemas = /* @__PURE__ */ new Set();
|
|
445
|
-
classifyContract(c, (s) => {
|
|
446
|
-
schemas.add(s);
|
|
447
|
-
return void 0;
|
|
448
|
-
});
|
|
449
|
-
const converted = /* @__PURE__ */ new Map();
|
|
450
|
-
await Promise.all(
|
|
451
|
-
[...schemas].map(
|
|
452
|
-
(s) => toJsonSchema(s).then(
|
|
453
|
-
(j) => {
|
|
454
|
-
converted.set(s, j);
|
|
455
|
-
},
|
|
456
|
-
() => {
|
|
457
|
-
}
|
|
458
|
-
// unsupported vendor / missing per-vendor converter -> structure-only for this entry
|
|
459
|
-
)
|
|
460
|
-
)
|
|
461
|
-
);
|
|
462
|
-
return classifyContract(c, (s) => converted.get(s));
|
|
463
|
-
}
|
|
464
|
-
function safeSnapshot(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
465
|
-
if (value === null) return null;
|
|
466
|
-
const t = typeof value;
|
|
467
|
-
if (t === "bigint") return `${value.toString()}n`;
|
|
468
|
-
if (t === "function") return "[Function]";
|
|
469
|
-
if (t === "symbol") return value.toString();
|
|
470
|
-
if (t !== "object") return value;
|
|
471
|
-
const obj = value;
|
|
472
|
-
if (obj instanceof Date) return obj.toISOString();
|
|
473
|
-
if (seen.has(obj)) return "[Circular]";
|
|
474
|
-
if (depth >= 6) return "[MaxDepth]";
|
|
475
|
-
seen.add(obj);
|
|
476
|
-
try {
|
|
477
|
-
if (Array.isArray(obj)) return obj.slice(0, 1e3).map((v) => safeSnapshot(v, depth + 1, seen));
|
|
478
|
-
const ctor = Object.getPrototypeOf(obj)?.constructor?.name;
|
|
479
|
-
const out = {};
|
|
480
|
-
if (ctor && ctor !== "Object") out["#type"] = ctor;
|
|
481
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
482
|
-
out[k] = inspectorRedact.has(k) ? "[Redacted]" : safeSnapshot(v, depth + 1, seen);
|
|
465
|
+
const reservedBridges = /* @__PURE__ */ new WeakMap();
|
|
466
|
+
async function onReservedFrame(conn, frame) {
|
|
467
|
+
const serving = reservedServing.get(conn.role);
|
|
468
|
+
if (!serving) return;
|
|
469
|
+
if (frame.t === "req") {
|
|
470
|
+
const handler = serving.handlers[frame.m];
|
|
471
|
+
if (!handler) {
|
|
472
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
|
|
473
|
+
return;
|
|
483
474
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const inspectorHandlers = {
|
|
490
|
-
getContract: () => buildInspectedContract(),
|
|
491
|
-
getTopology: async () => presenceOrThrow().topology(),
|
|
492
|
-
listConnections: async () => presenceOrThrow().list(),
|
|
493
|
-
getNode: async () => ({ nodeId: instanceId, nodeName, rooms: localRooms(), topics: localTopics() }),
|
|
494
|
-
getConn: async (input) => {
|
|
495
|
-
const id = input?.id;
|
|
496
|
-
if (!id) throw new SuperLineError("BAD_REQUEST", "getConn requires an id");
|
|
497
|
-
const local = [...conns].find((cn) => cn.id === id);
|
|
498
|
-
if (local) {
|
|
499
|
-
return {
|
|
500
|
-
descriptor: buildDescriptor(local),
|
|
501
|
-
ctx: safeSnapshot(local.ctx),
|
|
502
|
-
data: safeSnapshot(local.data),
|
|
503
|
-
ctxAvailable: true
|
|
504
|
-
};
|
|
475
|
+
try {
|
|
476
|
+
conn.send({ t: "res", i: frame.i, d: await handler(frame.d, conn) });
|
|
477
|
+
} catch (err) {
|
|
478
|
+
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
479
|
+
conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
|
|
505
480
|
}
|
|
506
|
-
|
|
507
|
-
if (!
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
listStores: async () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
|
|
511
|
-
listResources: async (input) => {
|
|
512
|
-
const store = storeMap[input.store];
|
|
513
|
-
if (!store) throw new SuperLineError("NOT_FOUND", `Unknown store: ${input.store}`);
|
|
514
|
-
return store.list();
|
|
515
|
-
},
|
|
516
|
-
readResource: async (input) => {
|
|
517
|
-
const { store: name, id } = input;
|
|
518
|
-
const store = storeMap[name];
|
|
519
|
-
if (!store) throw new SuperLineError("NOT_FOUND", `Unknown store: ${name}`);
|
|
520
|
-
const resource = await store.read(id);
|
|
521
|
-
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
522
|
-
let data = resource.data;
|
|
523
|
-
if (store.open) {
|
|
524
|
-
const replica = store.open(id);
|
|
525
|
-
data = replica.getSnapshot();
|
|
526
|
-
replica.close();
|
|
481
|
+
} else if (frame.t === "sub") {
|
|
482
|
+
if (!isSubscribeTopic(serving.connection.contract, frame.c)) {
|
|
483
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
|
|
484
|
+
return;
|
|
527
485
|
}
|
|
528
|
-
|
|
486
|
+
let bridges = reservedBridges.get(conn);
|
|
487
|
+
if (!bridges) {
|
|
488
|
+
bridges = /* @__PURE__ */ new Map();
|
|
489
|
+
reservedBridges.set(conn, bridges);
|
|
490
|
+
}
|
|
491
|
+
bridges.get(frame.c)?.();
|
|
492
|
+
bridges.set(frame.c, serving.ctx.channel(frame.c).subscribe((data) => conn.send({ t: "pub", c: frame.c, d: data })));
|
|
493
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
494
|
+
} else if (frame.t === "unsub") {
|
|
495
|
+
const bridges = reservedBridges.get(conn);
|
|
496
|
+
bridges?.get(frame.c)?.();
|
|
497
|
+
bridges?.delete(frame.c);
|
|
529
498
|
}
|
|
530
|
-
}
|
|
499
|
+
}
|
|
500
|
+
function isSubscribeTopic(contract2, name) {
|
|
501
|
+
const isTopicDef = (def) => !!def && typeof def === "object" && "subscribe" in def && def.subscribe === true;
|
|
502
|
+
if (isTopicDef(contract2.shared?.serverToClient?.[name])) return true;
|
|
503
|
+
for (const role of Object.keys(contract2.roles)) if (isTopicDef(contract2.roles[role]?.serverToClient?.[name])) return true;
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
531
506
|
const authHook = async (handshake) => {
|
|
532
507
|
const auth = await opts.authenticate(handshake);
|
|
533
508
|
return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
|
|
@@ -535,11 +510,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
535
510
|
function acceptConn(raw, auth) {
|
|
536
511
|
const role = auth.role;
|
|
537
512
|
const ctx = auth.ctx;
|
|
538
|
-
const
|
|
539
|
-
if (inspector && !inspectorEnabled) {
|
|
540
|
-
raw.close();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
513
|
+
const isReserved = reservedRoles.has(role);
|
|
543
514
|
const connId = randomUUID();
|
|
544
515
|
const conn = new Conn(
|
|
545
516
|
raw,
|
|
@@ -547,17 +518,22 @@ function createSuperLineServer(contract, opts) {
|
|
|
547
518
|
role,
|
|
548
519
|
ctx,
|
|
549
520
|
serializer,
|
|
550
|
-
|
|
521
|
+
taps.length ? (event, data) => emitTap({ type: "msg.event", target: connId, name: event, data }) : void 0
|
|
551
522
|
);
|
|
552
523
|
conn.transport = auth.transport;
|
|
553
524
|
conn.principal = resolvePrincipal(conn, opts.identify);
|
|
554
525
|
raw.onMessage((bytes) => {
|
|
555
526
|
void onMessage(conn, bytes);
|
|
556
527
|
});
|
|
557
|
-
if (
|
|
558
|
-
|
|
528
|
+
if (isReserved) {
|
|
529
|
+
reservedConns.add(conn);
|
|
559
530
|
raw.onClose(() => {
|
|
560
|
-
|
|
531
|
+
reservedConns.delete(conn);
|
|
532
|
+
const bridges = reservedBridges.get(conn);
|
|
533
|
+
if (bridges) {
|
|
534
|
+
for (const off of bridges.values()) off();
|
|
535
|
+
reservedBridges.delete(conn);
|
|
536
|
+
}
|
|
561
537
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
562
538
|
});
|
|
563
539
|
return;
|
|
@@ -568,18 +544,18 @@ function createSuperLineServer(contract, opts) {
|
|
|
568
544
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
569
545
|
void adapter.presence?.del(conn.id);
|
|
570
546
|
const goneUserId = opts.identify?.(conn);
|
|
571
|
-
|
|
547
|
+
emitTap({
|
|
572
548
|
type: "disconnect",
|
|
573
549
|
connId: conn.id,
|
|
574
550
|
nodeId: instanceId,
|
|
575
551
|
...goneUserId !== void 0 ? { userId: goneUserId } : {}
|
|
576
552
|
});
|
|
577
|
-
|
|
553
|
+
fireDisconnect(conn, ctx, code);
|
|
578
554
|
});
|
|
579
|
-
|
|
555
|
+
fireConnection(conn, ctx);
|
|
580
556
|
const descriptor = buildDescriptor(conn);
|
|
581
557
|
void adapter.presence?.set(descriptor);
|
|
582
|
-
|
|
558
|
+
emitTap({ type: "connect", descriptor });
|
|
583
559
|
void joinChannel(conn, CONN + conn.id);
|
|
584
560
|
const uid = opts.identify?.(conn);
|
|
585
561
|
if (uid !== void 0) void joinChannel(conn, USER + uid);
|
|
@@ -591,8 +567,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
591
567
|
} catch {
|
|
592
568
|
return;
|
|
593
569
|
}
|
|
594
|
-
if (
|
|
595
|
-
await
|
|
570
|
+
if (reservedConns.has(conn)) {
|
|
571
|
+
await onReservedFrame(conn, frame);
|
|
596
572
|
return;
|
|
597
573
|
}
|
|
598
574
|
if (frame.t === "req") await handleReq(conn, frame);
|
|
@@ -614,10 +590,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
614
590
|
}
|
|
615
591
|
}
|
|
616
592
|
for (const transport of opts.transports) {
|
|
617
|
-
void transport.start({ authenticate: authHook, onConnection: acceptConn });
|
|
593
|
+
void transport.start({ authenticate: authHook, onConnection: acceptConn, reserved });
|
|
618
594
|
}
|
|
619
595
|
function runMiddleware(info, terminal) {
|
|
620
|
-
const chain =
|
|
596
|
+
const chain = middlewareChain;
|
|
621
597
|
let last = -1;
|
|
622
598
|
const dispatch = (idx) => {
|
|
623
599
|
if (idx <= last) return Promise.reject(new Error("next() called multiple times"));
|
|
@@ -636,11 +612,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
636
612
|
responded = true;
|
|
637
613
|
});
|
|
638
614
|
} catch (err) {
|
|
639
|
-
|
|
615
|
+
fireError(err, info);
|
|
640
616
|
const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
|
|
641
617
|
if (!responded) conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
|
|
642
|
-
if (
|
|
643
|
-
|
|
618
|
+
if (taps.length && info.kind === "request")
|
|
619
|
+
emitTap({
|
|
644
620
|
type: "msg.response",
|
|
645
621
|
connId: conn.id,
|
|
646
622
|
name: info.name,
|
|
@@ -652,30 +628,30 @@ function createSuperLineServer(contract, opts) {
|
|
|
652
628
|
}
|
|
653
629
|
async function handleReq(conn, frame) {
|
|
654
630
|
const def = c.roles[conn.role]?.clientToServer?.[frame.m] ?? c.shared?.clientToServer?.[frame.m];
|
|
655
|
-
const handler = impl[conn.role]?.[frame.m] ?? impl.shared?.[frame.m];
|
|
631
|
+
const handler = impl[conn.role]?.[frame.m] ?? impl.shared?.[frame.m] ?? pluginHandlers[frame.m];
|
|
656
632
|
if (!def || !handler) {
|
|
657
633
|
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
|
|
658
634
|
return;
|
|
659
635
|
}
|
|
660
636
|
await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
|
|
661
637
|
const input = await validate(def.input, frame.d);
|
|
662
|
-
if (
|
|
663
|
-
|
|
638
|
+
if (taps.length)
|
|
639
|
+
emitTap({
|
|
664
640
|
type: "msg.request",
|
|
665
641
|
connId: conn.id,
|
|
666
642
|
role: conn.role,
|
|
667
643
|
name: frame.m,
|
|
668
|
-
input
|
|
644
|
+
input,
|
|
669
645
|
reqId: frame.i
|
|
670
646
|
});
|
|
671
647
|
const output = await handler(input, conn.ctx, conn);
|
|
672
|
-
if (
|
|
673
|
-
|
|
648
|
+
if (taps.length)
|
|
649
|
+
emitTap({
|
|
674
650
|
type: "msg.response",
|
|
675
651
|
connId: conn.id,
|
|
676
652
|
name: frame.m,
|
|
677
653
|
ok: true,
|
|
678
|
-
output
|
|
654
|
+
output,
|
|
679
655
|
reqId: frame.i
|
|
680
656
|
});
|
|
681
657
|
conn.send({ t: "res", i: frame.i, d: output });
|
|
@@ -711,7 +687,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
711
687
|
if (!resource.accessRules[principal]?.read)
|
|
712
688
|
throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
713
689
|
await joinChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
714
|
-
if (
|
|
690
|
+
if (taps.length) emitTap({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
715
691
|
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
716
692
|
});
|
|
717
693
|
}
|
|
@@ -737,14 +713,14 @@ function createSuperLineServer(contract, opts) {
|
|
|
737
713
|
if (!resource.accessRules[principal]?.write)
|
|
738
714
|
throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
739
715
|
await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
740
|
-
if (
|
|
741
|
-
|
|
716
|
+
if (taps.length)
|
|
717
|
+
emitTap({
|
|
742
718
|
type: "store.write",
|
|
743
719
|
store: frame.n,
|
|
744
720
|
id: frame.id,
|
|
745
721
|
origin: frame.o,
|
|
746
722
|
connId: conn.id,
|
|
747
|
-
data:
|
|
723
|
+
data: frame.u
|
|
748
724
|
});
|
|
749
725
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
750
726
|
});
|
|
@@ -752,7 +728,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
752
728
|
function handleStoreClose(conn, frame) {
|
|
753
729
|
if (!storeMap[frame.n]) return;
|
|
754
730
|
leaveChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
755
|
-
if (
|
|
731
|
+
if (taps.length) emitTap({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
756
732
|
}
|
|
757
733
|
for (const [name, store] of Object.entries(storeMap)) {
|
|
758
734
|
const isSelf = store.clustering === "self";
|
|
@@ -813,8 +789,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
813
789
|
leaveChannel(conn, channel);
|
|
814
790
|
},
|
|
815
791
|
broadcast(event, data) {
|
|
816
|
-
if (
|
|
817
|
-
|
|
792
|
+
if (taps.length)
|
|
793
|
+
emitTap({ type: "msg.broadcast", room: name, name: String(event), data });
|
|
818
794
|
void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
|
|
819
795
|
},
|
|
820
796
|
get size() {
|
|
@@ -827,7 +803,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
827
803
|
}
|
|
828
804
|
function publishTo(ns, name, data) {
|
|
829
805
|
const channel = TOPIC + ns + ":" + name;
|
|
830
|
-
if (
|
|
806
|
+
if (taps.length) emitTap({ type: "msg.publish", topic: name, data });
|
|
831
807
|
const busSet = busListeners.get(channel);
|
|
832
808
|
if (busSet) for (const cb of busSet) callBus(cb, data, instanceId, name);
|
|
833
809
|
void adapter.publish(channel, serializer.encode({ t: "pub", c: name, d: data, i: instanceId }));
|
|
@@ -836,7 +812,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
836
812
|
try {
|
|
837
813
|
cb(data, { from });
|
|
838
814
|
} catch (err) {
|
|
839
|
-
|
|
815
|
+
fireError(err, { kind: "event", name });
|
|
840
816
|
}
|
|
841
817
|
}
|
|
842
818
|
function deliverBus(payload, set) {
|
|
@@ -857,22 +833,62 @@ function createSuperLineServer(contract, opts) {
|
|
|
857
833
|
try {
|
|
858
834
|
data = await validate(schema, frame.d);
|
|
859
835
|
} catch (err) {
|
|
860
|
-
|
|
836
|
+
fireError(err, { kind: "event", name });
|
|
861
837
|
return;
|
|
862
838
|
}
|
|
863
839
|
}
|
|
864
840
|
for (const cb of set) callBus(cb, data, from, name);
|
|
865
841
|
})();
|
|
866
842
|
}
|
|
843
|
+
function handlePluginChannel(channel, payload) {
|
|
844
|
+
const set = pluginChannels.get(channel);
|
|
845
|
+
if (!set) return;
|
|
846
|
+
let frame;
|
|
847
|
+
try {
|
|
848
|
+
frame = serializer.decode(payload);
|
|
849
|
+
} catch {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (frame.i === instanceId) return;
|
|
853
|
+
for (const cb of set) callBus(cb, frame.d, frame.i, channel);
|
|
854
|
+
}
|
|
855
|
+
function pluginChannel(pluginName, name) {
|
|
856
|
+
const channel = PLUGIN + pluginName + ":" + name;
|
|
857
|
+
return {
|
|
858
|
+
publish(data) {
|
|
859
|
+
const set = pluginChannels.get(channel);
|
|
860
|
+
if (set) for (const cb of set) callBus(cb, data, instanceId, channel);
|
|
861
|
+
void adapter.publish(channel, serializer.encode({ i: instanceId, d: data }));
|
|
862
|
+
},
|
|
863
|
+
subscribe(handler) {
|
|
864
|
+
let set = pluginChannels.get(channel);
|
|
865
|
+
if (!set) {
|
|
866
|
+
set = /* @__PURE__ */ new Set();
|
|
867
|
+
pluginChannels.set(channel, set);
|
|
868
|
+
void adapter.subscribe(channel);
|
|
869
|
+
}
|
|
870
|
+
set.add(handler);
|
|
871
|
+
return () => {
|
|
872
|
+
const current = pluginChannels.get(channel);
|
|
873
|
+
if (!current) return;
|
|
874
|
+
current.delete(handler);
|
|
875
|
+
if (current.size === 0) {
|
|
876
|
+
pluginChannels.delete(channel);
|
|
877
|
+
void adapter.unsubscribe(channel);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
867
883
|
function personalTarget(channel) {
|
|
868
884
|
return {
|
|
869
885
|
emit(event, data) {
|
|
870
|
-
if (
|
|
871
|
-
|
|
886
|
+
if (taps.length)
|
|
887
|
+
emitTap({
|
|
872
888
|
type: "msg.event",
|
|
873
889
|
target: channel.slice(channel.indexOf(":") + 1),
|
|
874
890
|
name: event,
|
|
875
|
-
data
|
|
891
|
+
data
|
|
876
892
|
});
|
|
877
893
|
const env = { p: "emit", f: { t: "evt", e: event, d: data } };
|
|
878
894
|
void adapter.publish(channel, serializer.encode(env));
|
|
@@ -901,8 +917,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
901
917
|
},
|
|
902
918
|
{ once: true }
|
|
903
919
|
);
|
|
904
|
-
if (
|
|
905
|
-
|
|
920
|
+
if (taps.length)
|
|
921
|
+
emitTap({ type: "msg.serverRequest", target: id, name, input, reqId });
|
|
906
922
|
const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
|
|
907
923
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
908
924
|
});
|
|
@@ -917,7 +933,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
917
933
|
storeApi[name] = {
|
|
918
934
|
async create(id, data, accessRules) {
|
|
919
935
|
await store.create(id, data, accessRules);
|
|
920
|
-
if (
|
|
936
|
+
if (taps.length) emitTap({ type: "store.create", store: name, id });
|
|
921
937
|
},
|
|
922
938
|
read(id) {
|
|
923
939
|
return Promise.resolve(store.read(id));
|
|
@@ -925,20 +941,19 @@ function createSuperLineServer(contract, opts) {
|
|
|
925
941
|
async write(id, data, opts2) {
|
|
926
942
|
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
927
943
|
await store.apply({ id, update: data, origin });
|
|
928
|
-
if (
|
|
929
|
-
emitInspectorEvent({ type: "store.write", store: name, id, origin, data: safeSnapshot(data) });
|
|
944
|
+
if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
|
|
930
945
|
},
|
|
931
946
|
async grant(id, principal, perms) {
|
|
932
947
|
const r = await readOrThrow(id);
|
|
933
948
|
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
934
|
-
if (
|
|
949
|
+
if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
|
|
935
950
|
},
|
|
936
951
|
async revoke(id, principal) {
|
|
937
952
|
const r = await readOrThrow(id);
|
|
938
953
|
const next = { ...r.accessRules };
|
|
939
954
|
delete next[principal];
|
|
940
955
|
await store.setAccess(id, next);
|
|
941
|
-
if (
|
|
956
|
+
if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
|
|
942
957
|
},
|
|
943
958
|
async delete(id) {
|
|
944
959
|
await store.delete(id);
|
|
@@ -947,18 +962,21 @@ function createSuperLineServer(contract, opts) {
|
|
|
947
962
|
STORE + name + ":" + id,
|
|
948
963
|
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
949
964
|
);
|
|
950
|
-
if (
|
|
965
|
+
if (taps.length) emitTap({ type: "store.delete", store: name, id });
|
|
951
966
|
},
|
|
952
|
-
list() {
|
|
953
|
-
return Promise.resolve(store.list());
|
|
967
|
+
list(opts2) {
|
|
968
|
+
return Promise.resolve(store.list(opts2));
|
|
969
|
+
},
|
|
970
|
+
searchPrincipals(opts2) {
|
|
971
|
+
return Promise.resolve(store.searchPrincipals(opts2));
|
|
954
972
|
},
|
|
955
973
|
open(id, openOpts) {
|
|
956
974
|
if (!store.open)
|
|
957
975
|
throw new SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
|
|
958
976
|
const replica = store.open(id, openOpts);
|
|
959
|
-
if (!
|
|
977
|
+
if (!taps.length) return replica;
|
|
960
978
|
const origin = openOpts?.origin ?? SERVER_ORIGIN;
|
|
961
|
-
const emit = (data) =>
|
|
979
|
+
const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
|
|
962
980
|
return {
|
|
963
981
|
...replica,
|
|
964
982
|
set: (d) => {
|
|
@@ -977,6 +995,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
977
995
|
}
|
|
978
996
|
};
|
|
979
997
|
}
|
|
998
|
+
const pluginDisposers = [];
|
|
999
|
+
const pluginHandlers = {};
|
|
980
1000
|
const api = {
|
|
981
1001
|
nodeId: instanceId,
|
|
982
1002
|
nodeName,
|
|
@@ -1024,7 +1044,24 @@ function createSuperLineServer(contract, opts) {
|
|
|
1024
1044
|
return { emit: t.emit, disconnect: t.close };
|
|
1025
1045
|
},
|
|
1026
1046
|
implement(handlers) {
|
|
1027
|
-
|
|
1047
|
+
const map = handlers;
|
|
1048
|
+
const missing = [];
|
|
1049
|
+
const duplicate = [];
|
|
1050
|
+
const checkBlock = (block, defs) => {
|
|
1051
|
+
if (!defs) return;
|
|
1052
|
+
for (const key of Object.keys(defs)) {
|
|
1053
|
+
const inImpl = !!map[block]?.[key];
|
|
1054
|
+
const inPlugin = key in pluginHandlers;
|
|
1055
|
+
if (inImpl && inPlugin) duplicate.push(`${block}.${key}`);
|
|
1056
|
+
else if (!inImpl && !inPlugin) missing.push(`${block}.${key}`);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
checkBlock("shared", c.shared?.clientToServer);
|
|
1060
|
+
for (const role of Object.keys(c.roles)) checkBlock(role, c.roles[role]?.clientToServer);
|
|
1061
|
+
if (duplicate.length)
|
|
1062
|
+
throw new Error(`implement: these keys are also handled by a plugin \u2014 remove them: ${duplicate.join(", ")}`);
|
|
1063
|
+
if (missing.length) throw new Error(`implement: missing handler(s) for: ${missing.join(", ")}`);
|
|
1064
|
+
impl = map;
|
|
1028
1065
|
return api;
|
|
1029
1066
|
},
|
|
1030
1067
|
room,
|
|
@@ -1066,14 +1103,89 @@ function createSuperLineServer(contract, opts) {
|
|
|
1066
1103
|
async close() {
|
|
1067
1104
|
if (closing) return;
|
|
1068
1105
|
closing = true;
|
|
1106
|
+
for (const dispose of pluginDisposers) {
|
|
1107
|
+
try {
|
|
1108
|
+
dispose();
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1069
1112
|
if (hbTimer) clearInterval(hbTimer);
|
|
1070
1113
|
for (const conn of conns) conn.close();
|
|
1071
|
-
for (const conn of
|
|
1114
|
+
for (const conn of reservedConns) conn.close();
|
|
1072
1115
|
await adapter.presence?.clearNode(instanceId);
|
|
1073
1116
|
await adapter.close?.();
|
|
1074
1117
|
for (const transport of opts.transports) await transport.stop();
|
|
1075
1118
|
}
|
|
1076
1119
|
};
|
|
1120
|
+
function makePluginContext(pluginName) {
|
|
1121
|
+
return {
|
|
1122
|
+
nodeId: instanceId,
|
|
1123
|
+
nodeName,
|
|
1124
|
+
instanceId,
|
|
1125
|
+
serializer,
|
|
1126
|
+
contract: c,
|
|
1127
|
+
get conns() {
|
|
1128
|
+
return [...conns];
|
|
1129
|
+
},
|
|
1130
|
+
get local() {
|
|
1131
|
+
return api.local;
|
|
1132
|
+
},
|
|
1133
|
+
cluster: api.cluster,
|
|
1134
|
+
isOnline: (userId) => api.isOnline(userId),
|
|
1135
|
+
publish: (topic, data) => publishTo("shared", topic, data),
|
|
1136
|
+
subscribe: (topic, handler) => api.subscribe(topic, handler),
|
|
1137
|
+
toConn: (id) => {
|
|
1138
|
+
const t = api.toConn(id);
|
|
1139
|
+
return { emit: (event, data) => t.emit(event, data), close: t.close };
|
|
1140
|
+
},
|
|
1141
|
+
toUser: (userId) => {
|
|
1142
|
+
const t = api.toUser(userId);
|
|
1143
|
+
return { emit: (event, data) => t.emit(event, data), disconnect: t.disconnect };
|
|
1144
|
+
},
|
|
1145
|
+
room: (name) => {
|
|
1146
|
+
const r = room(name);
|
|
1147
|
+
return {
|
|
1148
|
+
add: (conn) => r.add(conn),
|
|
1149
|
+
remove: (conn) => r.remove(conn),
|
|
1150
|
+
broadcast: (event, data) => r.broadcast(event, data),
|
|
1151
|
+
get size() {
|
|
1152
|
+
return r.size;
|
|
1153
|
+
},
|
|
1154
|
+
get connections() {
|
|
1155
|
+
return r.connections;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
},
|
|
1159
|
+
store: (name) => api.store(name),
|
|
1160
|
+
storeInfos: () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
|
|
1161
|
+
describe: (conn) => buildDescriptor(conn),
|
|
1162
|
+
connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
|
|
1163
|
+
channel: (name) => pluginChannel(pluginName, name)
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
const contractRequestKeys = new Set(Object.keys(c.shared?.clientToServer ?? {}));
|
|
1167
|
+
for (const role of Object.keys(c.roles))
|
|
1168
|
+
for (const k of Object.keys(c.roles[role]?.clientToServer ?? {})) contractRequestKeys.add(k);
|
|
1169
|
+
for (const p of plugins) {
|
|
1170
|
+
const ctx = makePluginContext(p.name);
|
|
1171
|
+
if (p.handlers) {
|
|
1172
|
+
for (const [key, fn] of Object.entries(p.handlers(ctx))) {
|
|
1173
|
+
if (!contractRequestKeys.has(key))
|
|
1174
|
+
throw new Error(`Plugin '${p.name}' handles '${key}', which the contract has no request for \u2014 did you forget to merge its surface?`);
|
|
1175
|
+
if (key in pluginHandlers)
|
|
1176
|
+
throw new Error(`Plugin handler collision on '${key}' (already provided by another plugin)`);
|
|
1177
|
+
pluginHandlers[key] = fn;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (p.connection)
|
|
1181
|
+
reservedServing.set(p.connection.role, {
|
|
1182
|
+
connection: p.connection,
|
|
1183
|
+
handlers: p.connection.handlers?.(ctx) ?? {},
|
|
1184
|
+
ctx
|
|
1185
|
+
});
|
|
1186
|
+
const dispose = p.setup?.(ctx);
|
|
1187
|
+
if (dispose) pluginDisposers.push(dispose);
|
|
1188
|
+
}
|
|
1077
1189
|
return api;
|
|
1078
1190
|
}
|
|
1079
1191
|
export {
|