@super-line/server 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.cjs +239 -122
- package/dist/index.d.cts +60 -34
- package/dist/index.d.ts +60 -34
- package/dist/index.js +237 -122
- package/package.json +12 -8
package/dist/index.js
CHANGED
|
@@ -2,39 +2,39 @@ import "./chunk-MLKGABMK.js";
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
-
import { WebSocketServer } from "ws";
|
|
6
5
|
import {
|
|
7
6
|
jsonSerializer,
|
|
8
7
|
validate,
|
|
9
8
|
SuperLineError,
|
|
10
|
-
INSPECTOR_SUBPROTOCOL,
|
|
11
9
|
INSPECTOR_ROLE,
|
|
12
10
|
classifyContract
|
|
13
11
|
} from "@super-line/core";
|
|
14
12
|
|
|
15
13
|
// src/conn.ts
|
|
16
14
|
var Conn = class {
|
|
17
|
-
constructor(
|
|
18
|
-
this.
|
|
15
|
+
constructor(raw, id, role, ctx, serializer, onEmit) {
|
|
16
|
+
this.raw = raw;
|
|
19
17
|
this.id = id;
|
|
20
18
|
this.role = role;
|
|
21
19
|
this.ctx = ctx;
|
|
22
20
|
this.serializer = serializer;
|
|
23
|
-
this.backpressure = backpressure;
|
|
24
21
|
this.onEmit = onEmit;
|
|
25
22
|
}
|
|
26
|
-
|
|
23
|
+
raw;
|
|
27
24
|
id;
|
|
28
25
|
role;
|
|
29
26
|
ctx;
|
|
30
27
|
serializer;
|
|
31
|
-
backpressure;
|
|
32
28
|
onEmit;
|
|
33
29
|
/** Namespaced channels (rooms + topics) this connection belongs to. */
|
|
34
30
|
channels = /* @__PURE__ */ new Set();
|
|
35
31
|
/** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
|
|
36
32
|
data = {};
|
|
37
|
-
/**
|
|
33
|
+
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
34
|
+
transport;
|
|
35
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
36
|
+
principal;
|
|
37
|
+
/** When this connection was accepted (`Date.now()`). */
|
|
38
38
|
connectedAt = Date.now();
|
|
39
39
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
40
40
|
lastPingAt;
|
|
@@ -42,37 +42,33 @@ var Conn = class {
|
|
|
42
42
|
lastPongAt;
|
|
43
43
|
/** Pings sent since the last pong; drives reaping (managed by the server). */
|
|
44
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
|
-
}
|
|
56
45
|
/** Encode and send a frame (unicast, e.g. req/res). */
|
|
57
46
|
send(frame) {
|
|
58
|
-
if (this.
|
|
59
|
-
this.
|
|
47
|
+
if (!this.raw.writable) return;
|
|
48
|
+
this.raw.send(this.serializer.encode(frame));
|
|
60
49
|
}
|
|
61
50
|
/** Forward an already-encoded frame (fan-out path; encoded once at the source). */
|
|
62
51
|
sendRaw(payload) {
|
|
63
|
-
if (this.
|
|
64
|
-
this.
|
|
52
|
+
if (!this.raw.writable) return;
|
|
53
|
+
this.raw.send(payload);
|
|
65
54
|
}
|
|
66
55
|
/** Push an event to THIS connection (node-local). Scoped to the role's events. */
|
|
67
56
|
emit(event, data) {
|
|
68
57
|
this.onEmit?.(String(event), data);
|
|
69
58
|
this.send({ t: "evt", e: String(event), d: data });
|
|
70
59
|
}
|
|
71
|
-
/**
|
|
60
|
+
/** Graceful close of the underlying transport connection. */
|
|
72
61
|
close() {
|
|
73
|
-
this.
|
|
62
|
+
this.raw.close();
|
|
63
|
+
}
|
|
64
|
+
/** Hard close with no handshake — used by heartbeat reaping. */
|
|
65
|
+
terminate() {
|
|
66
|
+
this.raw.terminate();
|
|
74
67
|
}
|
|
75
68
|
};
|
|
69
|
+
function resolvePrincipal(conn, identify) {
|
|
70
|
+
return identify?.(conn) ?? conn.id;
|
|
71
|
+
}
|
|
76
72
|
|
|
77
73
|
// src/memory-adapter.ts
|
|
78
74
|
var MemoryBus = class {
|
|
@@ -189,6 +185,8 @@ var CONN = "c:";
|
|
|
189
185
|
var USER = "u:";
|
|
190
186
|
var REPLY = "reply:";
|
|
191
187
|
var INSPECT = "i:";
|
|
188
|
+
var STORE = "s:";
|
|
189
|
+
var SERVER_ORIGIN = "server";
|
|
192
190
|
function createSuperLineServer(contract, opts) {
|
|
193
191
|
const c = contract;
|
|
194
192
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
@@ -197,10 +195,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
197
195
|
const inspectorRedact = new Set(
|
|
198
196
|
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
199
197
|
);
|
|
200
|
-
const
|
|
201
|
-
noServer: true,
|
|
202
|
-
handleProtocols: (protocols) => inspectorEnabled && protocols.has(INSPECTOR_SUBPROTOCOL) ? INSPECTOR_SUBPROTOCOL : false
|
|
203
|
-
});
|
|
198
|
+
const storeMap = opts.stores ?? {};
|
|
204
199
|
const conns = /* @__PURE__ */ new Set();
|
|
205
200
|
const inspectorConns = /* @__PURE__ */ new Set();
|
|
206
201
|
const members = /* @__PURE__ */ new Map();
|
|
@@ -210,6 +205,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
210
205
|
const replyChannel = REPLY + instanceId;
|
|
211
206
|
let impl = {};
|
|
212
207
|
let closing = false;
|
|
208
|
+
let relaying = false;
|
|
213
209
|
let nextSReq = 1;
|
|
214
210
|
const originWaiters = /* @__PURE__ */ new Map();
|
|
215
211
|
const ownerRouting = /* @__PURE__ */ new Map();
|
|
@@ -225,6 +221,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
225
221
|
connectedAt: conn.connectedAt,
|
|
226
222
|
...userId !== void 0 ? { userId } : {},
|
|
227
223
|
rooms,
|
|
224
|
+
...conn.transport !== void 0 ? { transport: conn.transport } : {},
|
|
228
225
|
...opts.describeConn?.(conn)
|
|
229
226
|
};
|
|
230
227
|
}
|
|
@@ -241,6 +238,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
241
238
|
handlePersonal(channel, payload);
|
|
242
239
|
return;
|
|
243
240
|
}
|
|
241
|
+
if (channel.startsWith(STORE)) {
|
|
242
|
+
handleStoreRelay(channel, payload);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
244
245
|
const set = members.get(channel);
|
|
245
246
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
246
247
|
const busSet = busListeners.get(channel);
|
|
@@ -320,12 +321,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
320
321
|
void adapter.presence?.beat(instanceId);
|
|
321
322
|
for (const conn of conns) {
|
|
322
323
|
if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
|
|
323
|
-
conn.
|
|
324
|
+
conn.terminate();
|
|
324
325
|
continue;
|
|
325
326
|
}
|
|
326
327
|
conn.missedPongs++;
|
|
327
328
|
conn.lastPingAt = now;
|
|
328
|
-
conn.
|
|
329
|
+
conn.send({ t: "ping" });
|
|
329
330
|
}
|
|
330
331
|
}, hb.interval ?? 3e4);
|
|
331
332
|
hbTimer.unref?.();
|
|
@@ -493,94 +494,66 @@ function createSuperLineServer(contract, opts) {
|
|
|
493
494
|
return { descriptor: remote, ctxAvailable: false };
|
|
494
495
|
}
|
|
495
496
|
};
|
|
496
|
-
|
|
497
|
-
opts.
|
|
498
|
-
|
|
497
|
+
const authHook = async (handshake) => {
|
|
498
|
+
const auth = await opts.authenticate(handshake);
|
|
499
|
+
return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
|
|
500
|
+
};
|
|
501
|
+
function acceptConn(raw, auth) {
|
|
502
|
+
const role = auth.role;
|
|
503
|
+
const ctx = auth.ctx;
|
|
504
|
+
const inspector = role === INSPECTOR_ROLE;
|
|
505
|
+
if (inspector && !inspectorEnabled) {
|
|
506
|
+
raw.close();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const connId = randomUUID();
|
|
510
|
+
const conn = new Conn(
|
|
511
|
+
raw,
|
|
512
|
+
connId,
|
|
513
|
+
role,
|
|
514
|
+
ctx,
|
|
515
|
+
serializer,
|
|
516
|
+
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
517
|
+
);
|
|
518
|
+
conn.transport = auth.transport;
|
|
519
|
+
conn.principal = resolvePrincipal(conn, opts.identify);
|
|
520
|
+
raw.onMessage((bytes) => {
|
|
521
|
+
void onMessage(conn, bytes);
|
|
499
522
|
});
|
|
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
|
-
}
|
|
508
|
-
async function handleUpgrade(req, socket, head) {
|
|
509
|
-
if (opts.path) {
|
|
510
|
-
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
511
|
-
if (pathname !== opts.path) return;
|
|
512
|
-
}
|
|
513
|
-
const inspector = isInspectorRequest(req);
|
|
514
|
-
let role;
|
|
515
|
-
let ctx;
|
|
516
523
|
if (inspector) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
let auth;
|
|
521
|
-
try {
|
|
522
|
-
auth = await opts.authenticate(req);
|
|
523
|
-
} catch {
|
|
524
|
-
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
525
|
-
socket.destroy();
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
role = auth.role;
|
|
529
|
-
ctx = auth.ctx;
|
|
530
|
-
}
|
|
531
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
532
|
-
const connId = randomUUID();
|
|
533
|
-
const conn = new Conn(
|
|
534
|
-
ws,
|
|
535
|
-
connId,
|
|
536
|
-
role,
|
|
537
|
-
ctx,
|
|
538
|
-
serializer,
|
|
539
|
-
opts.backpressure,
|
|
540
|
-
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
541
|
-
);
|
|
542
|
-
ws.on("message", (data, isBinary) => {
|
|
543
|
-
void onMessage(conn, data, isBinary);
|
|
544
|
-
});
|
|
545
|
-
if (inspector) {
|
|
546
|
-
inspectorConns.add(conn);
|
|
547
|
-
ws.on("close", () => {
|
|
548
|
-
inspectorConns.delete(conn);
|
|
549
|
-
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
550
|
-
});
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
conns.add(conn);
|
|
554
|
-
ws.on("pong", () => {
|
|
555
|
-
conn.lastPongAt = Date.now();
|
|
556
|
-
conn.missedPongs = 0;
|
|
557
|
-
});
|
|
558
|
-
ws.on("close", (code) => {
|
|
559
|
-
conns.delete(conn);
|
|
524
|
+
inspectorConns.add(conn);
|
|
525
|
+
raw.onClose(() => {
|
|
526
|
+
inspectorConns.delete(conn);
|
|
560
527
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
561
|
-
void adapter.presence?.del(conn.id);
|
|
562
|
-
const goneUserId = opts.identify?.(conn);
|
|
563
|
-
emitInspectorEvent({
|
|
564
|
-
type: "disconnect",
|
|
565
|
-
connId: conn.id,
|
|
566
|
-
nodeId: instanceId,
|
|
567
|
-
...goneUserId !== void 0 ? { userId: goneUserId } : {}
|
|
568
|
-
});
|
|
569
|
-
opts.onDisconnect?.(conn, ctx, code);
|
|
570
528
|
});
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
conns.add(conn);
|
|
532
|
+
raw.onClose((code) => {
|
|
533
|
+
conns.delete(conn);
|
|
534
|
+
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
535
|
+
void adapter.presence?.del(conn.id);
|
|
536
|
+
const goneUserId = opts.identify?.(conn);
|
|
537
|
+
emitInspectorEvent({
|
|
538
|
+
type: "disconnect",
|
|
539
|
+
connId: conn.id,
|
|
540
|
+
nodeId: instanceId,
|
|
541
|
+
...goneUserId !== void 0 ? { userId: goneUserId } : {}
|
|
542
|
+
});
|
|
543
|
+
opts.onDisconnect?.(conn, ctx, code);
|
|
578
544
|
});
|
|
579
|
-
|
|
580
|
-
|
|
545
|
+
opts.onConnection?.(conn, ctx);
|
|
546
|
+
const descriptor = buildDescriptor(conn);
|
|
547
|
+
void adapter.presence?.set(descriptor);
|
|
548
|
+
emitInspectorEvent({ type: "connect", descriptor });
|
|
549
|
+
void joinChannel(conn, CONN + conn.id);
|
|
550
|
+
const uid = opts.identify?.(conn);
|
|
551
|
+
if (uid !== void 0) void joinChannel(conn, USER + uid);
|
|
552
|
+
}
|
|
553
|
+
async function onMessage(conn, bytes) {
|
|
581
554
|
let frame;
|
|
582
555
|
try {
|
|
583
|
-
frame = serializer.decode(
|
|
556
|
+
frame = serializer.decode(bytes);
|
|
584
557
|
} catch {
|
|
585
558
|
return;
|
|
586
559
|
}
|
|
@@ -593,12 +566,22 @@ function createSuperLineServer(contract, opts) {
|
|
|
593
566
|
else if (frame.t === "unsub") {
|
|
594
567
|
const ns = topicNamespace(conn.role, frame.c);
|
|
595
568
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
596
|
-
} else if (frame.t === "
|
|
569
|
+
} else if (frame.t === "sopen") await handleStoreOpen(conn, frame);
|
|
570
|
+
else if (frame.t === "srd") await handleStoreRead(conn, frame);
|
|
571
|
+
else if (frame.t === "swr") await handleStoreWrite(conn, frame);
|
|
572
|
+
else if (frame.t === "sclose") handleStoreClose(conn, frame);
|
|
573
|
+
else if (frame.t === "sres") {
|
|
597
574
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
598
575
|
} else if (frame.t === "serr") {
|
|
599
576
|
await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
|
|
577
|
+
} else if (frame.t === "pong") {
|
|
578
|
+
conn.lastPongAt = Date.now();
|
|
579
|
+
conn.missedPongs = 0;
|
|
600
580
|
}
|
|
601
581
|
}
|
|
582
|
+
for (const transport of opts.transports) {
|
|
583
|
+
void transport.start({ authenticate: authHook, onConnection: acceptConn });
|
|
584
|
+
}
|
|
602
585
|
function runMiddleware(info, terminal) {
|
|
603
586
|
const chain = opts.use ?? [];
|
|
604
587
|
let last = -1;
|
|
@@ -676,6 +659,100 @@ function createSuperLineServer(contract, opts) {
|
|
|
676
659
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
677
660
|
});
|
|
678
661
|
}
|
|
662
|
+
function storeOrErr(conn, id, name) {
|
|
663
|
+
const store = storeMap[name];
|
|
664
|
+
if (!store) conn.send({ t: "err", i: id, code: "NOT_FOUND", m: `Unknown store: ${name}` });
|
|
665
|
+
return store;
|
|
666
|
+
}
|
|
667
|
+
async function handleStoreOpen(conn, frame) {
|
|
668
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
669
|
+
if (!store) return;
|
|
670
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
671
|
+
const resource = await store.read(frame.id);
|
|
672
|
+
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
673
|
+
const principal = conn.principal ?? conn.id;
|
|
674
|
+
if (!resource.accessRules[principal]?.read)
|
|
675
|
+
throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
676
|
+
await joinChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
677
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
678
|
+
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async function handleStoreRead(conn, frame) {
|
|
682
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
683
|
+
if (!store) return;
|
|
684
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
685
|
+
const resource = await store.read(frame.id);
|
|
686
|
+
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
687
|
+
const principal = conn.principal ?? conn.id;
|
|
688
|
+
if (!resource.accessRules[principal]?.read)
|
|
689
|
+
throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
690
|
+
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
async function handleStoreWrite(conn, frame) {
|
|
694
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
695
|
+
if (!store) return;
|
|
696
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
697
|
+
const resource = await store.read(frame.id);
|
|
698
|
+
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
699
|
+
const principal = conn.principal ?? conn.id;
|
|
700
|
+
if (!resource.accessRules[principal]?.write)
|
|
701
|
+
throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
702
|
+
await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
703
|
+
if (inspectorEnabled)
|
|
704
|
+
emitInspectorEvent({
|
|
705
|
+
type: "store.write",
|
|
706
|
+
store: frame.n,
|
|
707
|
+
id: frame.id,
|
|
708
|
+
origin: frame.o,
|
|
709
|
+
connId: conn.id,
|
|
710
|
+
data: safeSnapshot(frame.u)
|
|
711
|
+
});
|
|
712
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function handleStoreClose(conn, frame) {
|
|
716
|
+
if (!storeMap[frame.n]) return;
|
|
717
|
+
leaveChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
718
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
719
|
+
}
|
|
720
|
+
for (const [name, store] of Object.entries(storeMap)) {
|
|
721
|
+
store.onChange((change) => {
|
|
722
|
+
if (relaying) return;
|
|
723
|
+
void adapter.publish(
|
|
724
|
+
STORE + name + ":" + change.id,
|
|
725
|
+
serializer.encode({
|
|
726
|
+
t: "sch",
|
|
727
|
+
n: name,
|
|
728
|
+
id: change.id,
|
|
729
|
+
u: change.update,
|
|
730
|
+
o: change.origin,
|
|
731
|
+
nd: instanceId
|
|
732
|
+
})
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function handleStoreRelay(channel, payload) {
|
|
737
|
+
const set = members.get(channel);
|
|
738
|
+
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
739
|
+
let frame;
|
|
740
|
+
try {
|
|
741
|
+
frame = serializer.decode(payload);
|
|
742
|
+
} catch {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (frame.nd === instanceId) return;
|
|
746
|
+
const store = storeMap[frame.n];
|
|
747
|
+
if (!store || store.clustering !== "relay") return;
|
|
748
|
+
relaying = true;
|
|
749
|
+
try {
|
|
750
|
+
void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
751
|
+
} catch {
|
|
752
|
+
} finally {
|
|
753
|
+
relaying = false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
679
756
|
function room(name) {
|
|
680
757
|
const channel = ROOM + name;
|
|
681
758
|
return {
|
|
@@ -780,6 +857,45 @@ function createSuperLineServer(contract, opts) {
|
|
|
780
857
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
781
858
|
});
|
|
782
859
|
}
|
|
860
|
+
const storeApi = {};
|
|
861
|
+
for (const [name, store] of Object.entries(storeMap)) {
|
|
862
|
+
const readOrThrow = async (id) => {
|
|
863
|
+
const r = await store.read(id);
|
|
864
|
+
if (!r) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
865
|
+
return r;
|
|
866
|
+
};
|
|
867
|
+
storeApi[name] = {
|
|
868
|
+
async create(id, data, accessRules) {
|
|
869
|
+
await store.create(id, data, accessRules);
|
|
870
|
+
},
|
|
871
|
+
read(id) {
|
|
872
|
+
return Promise.resolve(store.read(id));
|
|
873
|
+
},
|
|
874
|
+
async write(id, data) {
|
|
875
|
+
await store.apply({ id, update: data, origin: SERVER_ORIGIN });
|
|
876
|
+
if (inspectorEnabled)
|
|
877
|
+
emitInspectorEvent({ type: "store.write", store: name, id, origin: SERVER_ORIGIN, data: safeSnapshot(data) });
|
|
878
|
+
},
|
|
879
|
+
async grant(id, principal, perms) {
|
|
880
|
+
const r = await readOrThrow(id);
|
|
881
|
+
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
882
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.grant", store: name, id, principal, perms });
|
|
883
|
+
},
|
|
884
|
+
async revoke(id, principal) {
|
|
885
|
+
const r = await readOrThrow(id);
|
|
886
|
+
const next = { ...r.accessRules };
|
|
887
|
+
delete next[principal];
|
|
888
|
+
await store.setAccess(id, next);
|
|
889
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.revoke", store: name, id, principal });
|
|
890
|
+
},
|
|
891
|
+
async delete(id) {
|
|
892
|
+
await store.delete(id);
|
|
893
|
+
},
|
|
894
|
+
list() {
|
|
895
|
+
return Promise.resolve(store.list());
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
783
899
|
const api = {
|
|
784
900
|
nodeId: instanceId,
|
|
785
901
|
nodeName,
|
|
@@ -861,6 +977,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
861
977
|
}
|
|
862
978
|
};
|
|
863
979
|
},
|
|
980
|
+
store(name) {
|
|
981
|
+
const handle = storeApi[name];
|
|
982
|
+
if (!handle) throw new SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
|
|
983
|
+
return handle;
|
|
984
|
+
},
|
|
864
985
|
async close() {
|
|
865
986
|
if (closing) return;
|
|
866
987
|
closing = true;
|
|
@@ -869,21 +990,15 @@ function createSuperLineServer(contract, opts) {
|
|
|
869
990
|
for (const conn of inspectorConns) conn.close();
|
|
870
991
|
await adapter.presence?.clearNode(instanceId);
|
|
871
992
|
await adapter.close?.();
|
|
872
|
-
|
|
873
|
-
wss.close(() => resolve());
|
|
874
|
-
});
|
|
993
|
+
for (const transport of opts.transports) await transport.stop();
|
|
875
994
|
}
|
|
876
995
|
};
|
|
877
996
|
return api;
|
|
878
997
|
}
|
|
879
|
-
function toWire(data, _isBinary) {
|
|
880
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
881
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
882
|
-
return data;
|
|
883
|
-
}
|
|
884
998
|
export {
|
|
885
999
|
Conn,
|
|
886
1000
|
MemoryBus,
|
|
887
1001
|
createInMemoryAdapter,
|
|
888
|
-
createSuperLineServer
|
|
1002
|
+
createSuperLineServer,
|
|
1003
|
+
resolvePrincipal
|
|
889
1004
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-line/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typesafe WebSocket server for super-line — rooms, topics, middleware, pluggable adapters.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,8 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"
|
|
51
|
-
"@super-line/core": "^0.3.0"
|
|
50
|
+
"@super-line/core": "^0.5.0"
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
@@ -60,18 +59,23 @@
|
|
|
60
59
|
"@libp2p/memory": "^2.0.22",
|
|
61
60
|
"@libp2p/tcp": "^11.0.22",
|
|
62
61
|
"@types/ws": "^8.5.13",
|
|
62
|
+
"eventsource": "^3.0.2",
|
|
63
63
|
"ioredis": "^5.4.2",
|
|
64
|
+
"ws": "^8.18.0",
|
|
64
65
|
"libp2p": "^3.3.4",
|
|
65
66
|
"rabbitmq-client": "^5.0.0",
|
|
66
67
|
"testcontainers": "^10.13.2",
|
|
67
68
|
"zeromq": "^6.0.0",
|
|
68
69
|
"zod": "^3.24.1",
|
|
69
70
|
"zod-to-json-schema": "^3.25.2",
|
|
70
|
-
"@super-line/adapter-
|
|
71
|
-
"@super-line/adapter-
|
|
72
|
-
"@super-line/adapter-
|
|
73
|
-
"@super-line/
|
|
74
|
-
"@super-line/
|
|
71
|
+
"@super-line/adapter-libp2p": "0.5.0",
|
|
72
|
+
"@super-line/adapter-zeromq": "0.5.0",
|
|
73
|
+
"@super-line/adapter-rabbitmq": "0.5.0",
|
|
74
|
+
"@super-line/transport-http": "0.5.0",
|
|
75
|
+
"@super-line/transport-loopback": "0.5.0",
|
|
76
|
+
"@super-line/adapter-redis": "0.5.0",
|
|
77
|
+
"@super-line/client": "0.5.0",
|
|
78
|
+
"@super-line/transport-websocket": "0.5.0"
|
|
75
79
|
},
|
|
76
80
|
"optionalDependencies": {
|
|
77
81
|
"@standard-community/standard-json": "^0.3.5"
|