@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/README.md
CHANGED
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
The server for [**super-line**](https://mertdogar.github.io/super-line/) — end-to-end typesafe WebSockets for TypeScript. Implements a shared contract over [`ws`](https://www.npmjs.com/package/ws): role-keyed handlers, rooms, topics, middleware, lifecycle hooks, and node-to-node messaging.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
pnpm add @super-line/core @super-line/server zod
|
|
6
|
+
pnpm add @super-line/core @super-line/server @super-line/transport-websocket zod
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
import http from 'node:http'
|
|
11
11
|
import { createSuperLineServer } from '@super-line/server'
|
|
12
|
+
import { webSocketServerTransport } from '@super-line/transport-websocket'
|
|
12
13
|
import { api } from './contract'
|
|
13
14
|
|
|
14
15
|
const server = http.createServer()
|
|
15
16
|
const srv = createSuperLineServer(api, {
|
|
16
|
-
server,
|
|
17
|
-
authenticate: (
|
|
17
|
+
transports: [webSocketServerTransport({ server })],
|
|
18
|
+
authenticate: (h) => ({ role: 'user' as const, ctx: { id: '1' } }), // throw -> 401
|
|
18
19
|
})
|
|
19
20
|
|
|
20
21
|
srv.implement({
|
|
@@ -29,7 +30,7 @@ srv.implement({
|
|
|
29
30
|
server.listen(3000)
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
Authenticate returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
|
|
33
|
+
Authenticate receives the `Handshake` (`{ transport, headers, query, peer?, raw }`) and returns `{ role, ctx }`; cross-role calls are rejected with `NOT_FOUND`. The wire is carried by a pluggable transport — [`@super-line/transport-websocket`](https://www.npmjs.com/package/@super-line/transport-websocket) provides the WS transport shown above; other transports (HTTP/SSE, libp2p) are available — see the Transports guide. Scale across processes with [`@super-line/adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis).
|
|
33
34
|
|
|
34
35
|
- 📖 Docs: <https://mertdogar.github.io/super-line/>
|
|
35
36
|
- 📚 Guides: [roles & auth](https://mertdogar.github.io/super-line/guide/roles-auth), [events & rooms](https://mertdogar.github.io/super-line/guide/events-rooms)
|
package/dist/index.cjs
CHANGED
|
@@ -16272,36 +16272,38 @@ __export(index_exports, {
|
|
|
16272
16272
|
Conn: () => Conn,
|
|
16273
16273
|
MemoryBus: () => MemoryBus,
|
|
16274
16274
|
createInMemoryAdapter: () => createInMemoryAdapter,
|
|
16275
|
-
createSuperLineServer: () => createSuperLineServer
|
|
16275
|
+
createSuperLineServer: () => createSuperLineServer,
|
|
16276
|
+
resolvePrincipal: () => resolvePrincipal
|
|
16276
16277
|
});
|
|
16277
16278
|
module.exports = __toCommonJS(index_exports);
|
|
16278
16279
|
var import_node_crypto = require("crypto");
|
|
16279
|
-
var import_ws = require("ws");
|
|
16280
16280
|
var import_core2 = require("@super-line/core");
|
|
16281
16281
|
|
|
16282
16282
|
// src/conn.ts
|
|
16283
16283
|
var Conn = class {
|
|
16284
|
-
constructor(
|
|
16285
|
-
this.
|
|
16284
|
+
constructor(raw, id, role, ctx, serializer, onEmit) {
|
|
16285
|
+
this.raw = raw;
|
|
16286
16286
|
this.id = id;
|
|
16287
16287
|
this.role = role;
|
|
16288
16288
|
this.ctx = ctx;
|
|
16289
16289
|
this.serializer = serializer;
|
|
16290
|
-
this.backpressure = backpressure;
|
|
16291
16290
|
this.onEmit = onEmit;
|
|
16292
16291
|
}
|
|
16293
|
-
|
|
16292
|
+
raw;
|
|
16294
16293
|
id;
|
|
16295
16294
|
role;
|
|
16296
16295
|
ctx;
|
|
16297
16296
|
serializer;
|
|
16298
|
-
backpressure;
|
|
16299
16297
|
onEmit;
|
|
16300
16298
|
/** Namespaced channels (rooms + topics) this connection belongs to. */
|
|
16301
16299
|
channels = /* @__PURE__ */ new Set();
|
|
16302
16300
|
/** Mutable per-connection scratch state, typed per role by the contract's `data` schema. */
|
|
16303
16301
|
data = {};
|
|
16304
|
-
/**
|
|
16302
|
+
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
16303
|
+
transport;
|
|
16304
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
16305
|
+
principal;
|
|
16306
|
+
/** When this connection was accepted (`Date.now()`). */
|
|
16305
16307
|
connectedAt = Date.now();
|
|
16306
16308
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
16307
16309
|
lastPingAt;
|
|
@@ -16309,37 +16311,33 @@ var Conn = class {
|
|
|
16309
16311
|
lastPongAt;
|
|
16310
16312
|
/** Pings sent since the last pong; drives reaping (managed by the server). */
|
|
16311
16313
|
missedPongs = 0;
|
|
16312
|
-
// true => the frame was handled by the backpressure policy and must not be sent
|
|
16313
|
-
overBackpressure() {
|
|
16314
|
-
const bp = this.backpressure;
|
|
16315
|
-
if (!bp || this.ws.bufferedAmount <= bp.maxBufferedBytes) return false;
|
|
16316
|
-
if (bp.onExceed === "drop") {
|
|
16317
|
-
console.warn(`[super-line] dropping frame: conn ${this.id} over backpressure limit`);
|
|
16318
|
-
return true;
|
|
16319
|
-
}
|
|
16320
|
-
this.ws.close(1013);
|
|
16321
|
-
return true;
|
|
16322
|
-
}
|
|
16323
16314
|
/** Encode and send a frame (unicast, e.g. req/res). */
|
|
16324
16315
|
send(frame) {
|
|
16325
|
-
if (this.
|
|
16326
|
-
this.
|
|
16316
|
+
if (!this.raw.writable) return;
|
|
16317
|
+
this.raw.send(this.serializer.encode(frame));
|
|
16327
16318
|
}
|
|
16328
16319
|
/** Forward an already-encoded frame (fan-out path; encoded once at the source). */
|
|
16329
16320
|
sendRaw(payload) {
|
|
16330
|
-
if (this.
|
|
16331
|
-
this.
|
|
16321
|
+
if (!this.raw.writable) return;
|
|
16322
|
+
this.raw.send(payload);
|
|
16332
16323
|
}
|
|
16333
16324
|
/** Push an event to THIS connection (node-local). Scoped to the role's events. */
|
|
16334
16325
|
emit(event, data) {
|
|
16335
16326
|
this.onEmit?.(String(event), data);
|
|
16336
16327
|
this.send({ t: "evt", e: String(event), d: data });
|
|
16337
16328
|
}
|
|
16338
|
-
/**
|
|
16329
|
+
/** Graceful close of the underlying transport connection. */
|
|
16339
16330
|
close() {
|
|
16340
|
-
this.
|
|
16331
|
+
this.raw.close();
|
|
16332
|
+
}
|
|
16333
|
+
/** Hard close with no handshake — used by heartbeat reaping. */
|
|
16334
|
+
terminate() {
|
|
16335
|
+
this.raw.terminate();
|
|
16341
16336
|
}
|
|
16342
16337
|
};
|
|
16338
|
+
function resolvePrincipal(conn, identify) {
|
|
16339
|
+
return identify?.(conn) ?? conn.id;
|
|
16340
|
+
}
|
|
16343
16341
|
|
|
16344
16342
|
// src/memory-adapter.ts
|
|
16345
16343
|
var MemoryBus = class {
|
|
@@ -16456,6 +16454,8 @@ var CONN = "c:";
|
|
|
16456
16454
|
var USER = "u:";
|
|
16457
16455
|
var REPLY = "reply:";
|
|
16458
16456
|
var INSPECT = "i:";
|
|
16457
|
+
var STORE = "s:";
|
|
16458
|
+
var SERVER_ORIGIN = "server";
|
|
16459
16459
|
function createSuperLineServer(contract, opts) {
|
|
16460
16460
|
const c = contract;
|
|
16461
16461
|
const serializer = opts.serializer ?? import_core2.jsonSerializer;
|
|
@@ -16464,10 +16464,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16464
16464
|
const inspectorRedact = new Set(
|
|
16465
16465
|
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
16466
16466
|
);
|
|
16467
|
-
const
|
|
16468
|
-
noServer: true,
|
|
16469
|
-
handleProtocols: (protocols) => inspectorEnabled && protocols.has(import_core2.INSPECTOR_SUBPROTOCOL) ? import_core2.INSPECTOR_SUBPROTOCOL : false
|
|
16470
|
-
});
|
|
16467
|
+
const storeMap = opts.stores ?? {};
|
|
16471
16468
|
const conns = /* @__PURE__ */ new Set();
|
|
16472
16469
|
const inspectorConns = /* @__PURE__ */ new Set();
|
|
16473
16470
|
const members = /* @__PURE__ */ new Map();
|
|
@@ -16477,6 +16474,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16477
16474
|
const replyChannel = REPLY + instanceId;
|
|
16478
16475
|
let impl = {};
|
|
16479
16476
|
let closing = false;
|
|
16477
|
+
let relaying = false;
|
|
16480
16478
|
let nextSReq = 1;
|
|
16481
16479
|
const originWaiters = /* @__PURE__ */ new Map();
|
|
16482
16480
|
const ownerRouting = /* @__PURE__ */ new Map();
|
|
@@ -16492,6 +16490,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16492
16490
|
connectedAt: conn.connectedAt,
|
|
16493
16491
|
...userId !== void 0 ? { userId } : {},
|
|
16494
16492
|
rooms,
|
|
16493
|
+
...conn.transport !== void 0 ? { transport: conn.transport } : {},
|
|
16495
16494
|
...opts.describeConn?.(conn)
|
|
16496
16495
|
};
|
|
16497
16496
|
}
|
|
@@ -16508,6 +16507,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
16508
16507
|
handlePersonal(channel, payload);
|
|
16509
16508
|
return;
|
|
16510
16509
|
}
|
|
16510
|
+
if (channel.startsWith(STORE)) {
|
|
16511
|
+
handleStoreRelay(channel, payload);
|
|
16512
|
+
return;
|
|
16513
|
+
}
|
|
16511
16514
|
const set = members.get(channel);
|
|
16512
16515
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
16513
16516
|
const busSet = busListeners.get(channel);
|
|
@@ -16587,12 +16590,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
16587
16590
|
void adapter.presence?.beat(instanceId);
|
|
16588
16591
|
for (const conn of conns) {
|
|
16589
16592
|
if (hb.maxMissed != null && conn.missedPongs >= hb.maxMissed) {
|
|
16590
|
-
conn.
|
|
16593
|
+
conn.terminate();
|
|
16591
16594
|
continue;
|
|
16592
16595
|
}
|
|
16593
16596
|
conn.missedPongs++;
|
|
16594
16597
|
conn.lastPingAt = now;
|
|
16595
|
-
conn.
|
|
16598
|
+
conn.send({ t: "ping" });
|
|
16596
16599
|
}
|
|
16597
16600
|
}, hb.interval ?? 3e4);
|
|
16598
16601
|
hbTimer.unref?.();
|
|
@@ -16760,94 +16763,66 @@ function createSuperLineServer(contract, opts) {
|
|
|
16760
16763
|
return { descriptor: remote, ctxAvailable: false };
|
|
16761
16764
|
}
|
|
16762
16765
|
};
|
|
16763
|
-
|
|
16764
|
-
opts.
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
const
|
|
16771
|
-
if (!
|
|
16772
|
-
|
|
16773
|
-
|
|
16774
|
-
}
|
|
16775
|
-
async function handleUpgrade(req, socket, head) {
|
|
16776
|
-
if (opts.path) {
|
|
16777
|
-
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
16778
|
-
if (pathname !== opts.path) return;
|
|
16766
|
+
const authHook = async (handshake) => {
|
|
16767
|
+
const auth = await opts.authenticate(handshake);
|
|
16768
|
+
return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
|
|
16769
|
+
};
|
|
16770
|
+
function acceptConn(raw, auth) {
|
|
16771
|
+
const role = auth.role;
|
|
16772
|
+
const ctx = auth.ctx;
|
|
16773
|
+
const inspector = role === import_core2.INSPECTOR_ROLE;
|
|
16774
|
+
if (inspector && !inspectorEnabled) {
|
|
16775
|
+
raw.close();
|
|
16776
|
+
return;
|
|
16779
16777
|
}
|
|
16780
|
-
const
|
|
16781
|
-
|
|
16782
|
-
|
|
16778
|
+
const connId = (0, import_node_crypto.randomUUID)();
|
|
16779
|
+
const conn = new Conn(
|
|
16780
|
+
raw,
|
|
16781
|
+
connId,
|
|
16782
|
+
role,
|
|
16783
|
+
ctx,
|
|
16784
|
+
serializer,
|
|
16785
|
+
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
16786
|
+
);
|
|
16787
|
+
conn.transport = auth.transport;
|
|
16788
|
+
conn.principal = resolvePrincipal(conn, opts.identify);
|
|
16789
|
+
raw.onMessage((bytes) => {
|
|
16790
|
+
void onMessage(conn, bytes);
|
|
16791
|
+
});
|
|
16783
16792
|
if (inspector) {
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
let auth;
|
|
16788
|
-
try {
|
|
16789
|
-
auth = await opts.authenticate(req);
|
|
16790
|
-
} catch {
|
|
16791
|
-
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
16792
|
-
socket.destroy();
|
|
16793
|
-
return;
|
|
16794
|
-
}
|
|
16795
|
-
role = auth.role;
|
|
16796
|
-
ctx = auth.ctx;
|
|
16797
|
-
}
|
|
16798
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
16799
|
-
const connId = (0, import_node_crypto.randomUUID)();
|
|
16800
|
-
const conn = new Conn(
|
|
16801
|
-
ws,
|
|
16802
|
-
connId,
|
|
16803
|
-
role,
|
|
16804
|
-
ctx,
|
|
16805
|
-
serializer,
|
|
16806
|
-
opts.backpressure,
|
|
16807
|
-
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
16808
|
-
);
|
|
16809
|
-
ws.on("message", (data, isBinary) => {
|
|
16810
|
-
void onMessage(conn, data, isBinary);
|
|
16811
|
-
});
|
|
16812
|
-
if (inspector) {
|
|
16813
|
-
inspectorConns.add(conn);
|
|
16814
|
-
ws.on("close", () => {
|
|
16815
|
-
inspectorConns.delete(conn);
|
|
16816
|
-
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
16817
|
-
});
|
|
16818
|
-
return;
|
|
16819
|
-
}
|
|
16820
|
-
conns.add(conn);
|
|
16821
|
-
ws.on("pong", () => {
|
|
16822
|
-
conn.lastPongAt = Date.now();
|
|
16823
|
-
conn.missedPongs = 0;
|
|
16824
|
-
});
|
|
16825
|
-
ws.on("close", (code) => {
|
|
16826
|
-
conns.delete(conn);
|
|
16793
|
+
inspectorConns.add(conn);
|
|
16794
|
+
raw.onClose(() => {
|
|
16795
|
+
inspectorConns.delete(conn);
|
|
16827
16796
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
16828
|
-
void adapter.presence?.del(conn.id);
|
|
16829
|
-
const goneUserId = opts.identify?.(conn);
|
|
16830
|
-
emitInspectorEvent({
|
|
16831
|
-
type: "disconnect",
|
|
16832
|
-
connId: conn.id,
|
|
16833
|
-
nodeId: instanceId,
|
|
16834
|
-
...goneUserId !== void 0 ? { userId: goneUserId } : {}
|
|
16835
|
-
});
|
|
16836
|
-
opts.onDisconnect?.(conn, ctx, code);
|
|
16837
16797
|
});
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
const
|
|
16844
|
-
|
|
16798
|
+
return;
|
|
16799
|
+
}
|
|
16800
|
+
conns.add(conn);
|
|
16801
|
+
raw.onClose((code) => {
|
|
16802
|
+
conns.delete(conn);
|
|
16803
|
+
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
16804
|
+
void adapter.presence?.del(conn.id);
|
|
16805
|
+
const goneUserId = opts.identify?.(conn);
|
|
16806
|
+
emitInspectorEvent({
|
|
16807
|
+
type: "disconnect",
|
|
16808
|
+
connId: conn.id,
|
|
16809
|
+
nodeId: instanceId,
|
|
16810
|
+
...goneUserId !== void 0 ? { userId: goneUserId } : {}
|
|
16811
|
+
});
|
|
16812
|
+
opts.onDisconnect?.(conn, ctx, code);
|
|
16845
16813
|
});
|
|
16846
|
-
|
|
16847
|
-
|
|
16814
|
+
opts.onConnection?.(conn, ctx);
|
|
16815
|
+
const descriptor = buildDescriptor(conn);
|
|
16816
|
+
void adapter.presence?.set(descriptor);
|
|
16817
|
+
emitInspectorEvent({ type: "connect", descriptor });
|
|
16818
|
+
void joinChannel(conn, CONN + conn.id);
|
|
16819
|
+
const uid = opts.identify?.(conn);
|
|
16820
|
+
if (uid !== void 0) void joinChannel(conn, USER + uid);
|
|
16821
|
+
}
|
|
16822
|
+
async function onMessage(conn, bytes) {
|
|
16848
16823
|
let frame;
|
|
16849
16824
|
try {
|
|
16850
|
-
frame = serializer.decode(
|
|
16825
|
+
frame = serializer.decode(bytes);
|
|
16851
16826
|
} catch {
|
|
16852
16827
|
return;
|
|
16853
16828
|
}
|
|
@@ -16860,12 +16835,22 @@ function createSuperLineServer(contract, opts) {
|
|
|
16860
16835
|
else if (frame.t === "unsub") {
|
|
16861
16836
|
const ns = topicNamespace(conn.role, frame.c);
|
|
16862
16837
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
16863
|
-
} else if (frame.t === "
|
|
16838
|
+
} else if (frame.t === "sopen") await handleStoreOpen(conn, frame);
|
|
16839
|
+
else if (frame.t === "srd") await handleStoreRead(conn, frame);
|
|
16840
|
+
else if (frame.t === "swr") await handleStoreWrite(conn, frame);
|
|
16841
|
+
else if (frame.t === "sclose") handleStoreClose(conn, frame);
|
|
16842
|
+
else if (frame.t === "sres") {
|
|
16864
16843
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
16865
16844
|
} else if (frame.t === "serr") {
|
|
16866
16845
|
await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
|
|
16846
|
+
} else if (frame.t === "pong") {
|
|
16847
|
+
conn.lastPongAt = Date.now();
|
|
16848
|
+
conn.missedPongs = 0;
|
|
16867
16849
|
}
|
|
16868
16850
|
}
|
|
16851
|
+
for (const transport of opts.transports) {
|
|
16852
|
+
void transport.start({ authenticate: authHook, onConnection: acceptConn });
|
|
16853
|
+
}
|
|
16869
16854
|
function runMiddleware(info, terminal) {
|
|
16870
16855
|
const chain = opts.use ?? [];
|
|
16871
16856
|
let last = -1;
|
|
@@ -16943,6 +16928,100 @@ function createSuperLineServer(contract, opts) {
|
|
|
16943
16928
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
16944
16929
|
});
|
|
16945
16930
|
}
|
|
16931
|
+
function storeOrErr(conn, id, name) {
|
|
16932
|
+
const store = storeMap[name];
|
|
16933
|
+
if (!store) conn.send({ t: "err", i: id, code: "NOT_FOUND", m: `Unknown store: ${name}` });
|
|
16934
|
+
return store;
|
|
16935
|
+
}
|
|
16936
|
+
async function handleStoreOpen(conn, frame) {
|
|
16937
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
16938
|
+
if (!store) return;
|
|
16939
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
16940
|
+
const resource = await store.read(frame.id);
|
|
16941
|
+
if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
16942
|
+
const principal = conn.principal ?? conn.id;
|
|
16943
|
+
if (!resource.accessRules[principal]?.read)
|
|
16944
|
+
throw new import_core2.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
16945
|
+
await joinChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
16946
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
16947
|
+
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
16948
|
+
});
|
|
16949
|
+
}
|
|
16950
|
+
async function handleStoreRead(conn, frame) {
|
|
16951
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
16952
|
+
if (!store) return;
|
|
16953
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
16954
|
+
const resource = await store.read(frame.id);
|
|
16955
|
+
if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
16956
|
+
const principal = conn.principal ?? conn.id;
|
|
16957
|
+
if (!resource.accessRules[principal]?.read)
|
|
16958
|
+
throw new import_core2.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
16959
|
+
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
16960
|
+
});
|
|
16961
|
+
}
|
|
16962
|
+
async function handleStoreWrite(conn, frame) {
|
|
16963
|
+
const store = storeOrErr(conn, frame.i, frame.n);
|
|
16964
|
+
if (!store) return;
|
|
16965
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
16966
|
+
const resource = await store.read(frame.id);
|
|
16967
|
+
if (!resource) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
16968
|
+
const principal = conn.principal ?? conn.id;
|
|
16969
|
+
if (!resource.accessRules[principal]?.write)
|
|
16970
|
+
throw new import_core2.SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
16971
|
+
await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
16972
|
+
if (inspectorEnabled)
|
|
16973
|
+
emitInspectorEvent({
|
|
16974
|
+
type: "store.write",
|
|
16975
|
+
store: frame.n,
|
|
16976
|
+
id: frame.id,
|
|
16977
|
+
origin: frame.o,
|
|
16978
|
+
connId: conn.id,
|
|
16979
|
+
data: safeSnapshot(frame.u)
|
|
16980
|
+
});
|
|
16981
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
16982
|
+
});
|
|
16983
|
+
}
|
|
16984
|
+
function handleStoreClose(conn, frame) {
|
|
16985
|
+
if (!storeMap[frame.n]) return;
|
|
16986
|
+
leaveChannel(conn, STORE + frame.n + ":" + frame.id);
|
|
16987
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
16988
|
+
}
|
|
16989
|
+
for (const [name, store] of Object.entries(storeMap)) {
|
|
16990
|
+
store.onChange((change) => {
|
|
16991
|
+
if (relaying) return;
|
|
16992
|
+
void adapter.publish(
|
|
16993
|
+
STORE + name + ":" + change.id,
|
|
16994
|
+
serializer.encode({
|
|
16995
|
+
t: "sch",
|
|
16996
|
+
n: name,
|
|
16997
|
+
id: change.id,
|
|
16998
|
+
u: change.update,
|
|
16999
|
+
o: change.origin,
|
|
17000
|
+
nd: instanceId
|
|
17001
|
+
})
|
|
17002
|
+
);
|
|
17003
|
+
});
|
|
17004
|
+
}
|
|
17005
|
+
function handleStoreRelay(channel, payload) {
|
|
17006
|
+
const set = members.get(channel);
|
|
17007
|
+
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
17008
|
+
let frame;
|
|
17009
|
+
try {
|
|
17010
|
+
frame = serializer.decode(payload);
|
|
17011
|
+
} catch {
|
|
17012
|
+
return;
|
|
17013
|
+
}
|
|
17014
|
+
if (frame.nd === instanceId) return;
|
|
17015
|
+
const store = storeMap[frame.n];
|
|
17016
|
+
if (!store || store.clustering !== "relay") return;
|
|
17017
|
+
relaying = true;
|
|
17018
|
+
try {
|
|
17019
|
+
void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
17020
|
+
} catch {
|
|
17021
|
+
} finally {
|
|
17022
|
+
relaying = false;
|
|
17023
|
+
}
|
|
17024
|
+
}
|
|
16946
17025
|
function room(name) {
|
|
16947
17026
|
const channel = ROOM + name;
|
|
16948
17027
|
return {
|
|
@@ -17047,6 +17126,45 @@ function createSuperLineServer(contract, opts) {
|
|
|
17047
17126
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
17048
17127
|
});
|
|
17049
17128
|
}
|
|
17129
|
+
const storeApi = {};
|
|
17130
|
+
for (const [name, store] of Object.entries(storeMap)) {
|
|
17131
|
+
const readOrThrow = async (id) => {
|
|
17132
|
+
const r = await store.read(id);
|
|
17133
|
+
if (!r) throw new import_core2.SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
17134
|
+
return r;
|
|
17135
|
+
};
|
|
17136
|
+
storeApi[name] = {
|
|
17137
|
+
async create(id, data, accessRules) {
|
|
17138
|
+
await store.create(id, data, accessRules);
|
|
17139
|
+
},
|
|
17140
|
+
read(id) {
|
|
17141
|
+
return Promise.resolve(store.read(id));
|
|
17142
|
+
},
|
|
17143
|
+
async write(id, data) {
|
|
17144
|
+
await store.apply({ id, update: data, origin: SERVER_ORIGIN });
|
|
17145
|
+
if (inspectorEnabled)
|
|
17146
|
+
emitInspectorEvent({ type: "store.write", store: name, id, origin: SERVER_ORIGIN, data: safeSnapshot(data) });
|
|
17147
|
+
},
|
|
17148
|
+
async grant(id, principal, perms) {
|
|
17149
|
+
const r = await readOrThrow(id);
|
|
17150
|
+
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
17151
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.grant", store: name, id, principal, perms });
|
|
17152
|
+
},
|
|
17153
|
+
async revoke(id, principal) {
|
|
17154
|
+
const r = await readOrThrow(id);
|
|
17155
|
+
const next = { ...r.accessRules };
|
|
17156
|
+
delete next[principal];
|
|
17157
|
+
await store.setAccess(id, next);
|
|
17158
|
+
if (inspectorEnabled) emitInspectorEvent({ type: "store.revoke", store: name, id, principal });
|
|
17159
|
+
},
|
|
17160
|
+
async delete(id) {
|
|
17161
|
+
await store.delete(id);
|
|
17162
|
+
},
|
|
17163
|
+
list() {
|
|
17164
|
+
return Promise.resolve(store.list());
|
|
17165
|
+
}
|
|
17166
|
+
};
|
|
17167
|
+
}
|
|
17050
17168
|
const api = {
|
|
17051
17169
|
nodeId: instanceId,
|
|
17052
17170
|
nodeName,
|
|
@@ -17128,6 +17246,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
17128
17246
|
}
|
|
17129
17247
|
};
|
|
17130
17248
|
},
|
|
17249
|
+
store(name) {
|
|
17250
|
+
const handle = storeApi[name];
|
|
17251
|
+
if (!handle) throw new import_core2.SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
|
|
17252
|
+
return handle;
|
|
17253
|
+
},
|
|
17131
17254
|
async close() {
|
|
17132
17255
|
if (closing) return;
|
|
17133
17256
|
closing = true;
|
|
@@ -17136,22 +17259,16 @@ function createSuperLineServer(contract, opts) {
|
|
|
17136
17259
|
for (const conn of inspectorConns) conn.close();
|
|
17137
17260
|
await adapter.presence?.clearNode(instanceId);
|
|
17138
17261
|
await adapter.close?.();
|
|
17139
|
-
|
|
17140
|
-
wss.close(() => resolve());
|
|
17141
|
-
});
|
|
17262
|
+
for (const transport of opts.transports) await transport.stop();
|
|
17142
17263
|
}
|
|
17143
17264
|
};
|
|
17144
17265
|
return api;
|
|
17145
17266
|
}
|
|
17146
|
-
function toWire(data, _isBinary) {
|
|
17147
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
17148
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
17149
|
-
return data;
|
|
17150
|
-
}
|
|
17151
17267
|
// Annotate the CommonJS export names for ESM import in node:
|
|
17152
17268
|
0 && (module.exports = {
|
|
17153
17269
|
Conn,
|
|
17154
17270
|
MemoryBus,
|
|
17155
17271
|
createInMemoryAdapter,
|
|
17156
|
-
createSuperLineServer
|
|
17272
|
+
createSuperLineServer,
|
|
17273
|
+
resolvePrincipal
|
|
17157
17274
|
});
|