@super-line/server 0.4.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/dist/index.cjs +161 -3
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +159 -2
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -16272,7 +16272,8 @@ __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");
|
|
@@ -16300,6 +16301,8 @@ var Conn = class {
|
|
|
16300
16301
|
data = {};
|
|
16301
16302
|
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
16302
16303
|
transport;
|
|
16304
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
16305
|
+
principal;
|
|
16303
16306
|
/** When this connection was accepted (`Date.now()`). */
|
|
16304
16307
|
connectedAt = Date.now();
|
|
16305
16308
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
@@ -16332,6 +16335,9 @@ var Conn = class {
|
|
|
16332
16335
|
this.raw.terminate();
|
|
16333
16336
|
}
|
|
16334
16337
|
};
|
|
16338
|
+
function resolvePrincipal(conn, identify) {
|
|
16339
|
+
return identify?.(conn) ?? conn.id;
|
|
16340
|
+
}
|
|
16335
16341
|
|
|
16336
16342
|
// src/memory-adapter.ts
|
|
16337
16343
|
var MemoryBus = class {
|
|
@@ -16448,6 +16454,8 @@ var CONN = "c:";
|
|
|
16448
16454
|
var USER = "u:";
|
|
16449
16455
|
var REPLY = "reply:";
|
|
16450
16456
|
var INSPECT = "i:";
|
|
16457
|
+
var STORE = "s:";
|
|
16458
|
+
var SERVER_ORIGIN = "server";
|
|
16451
16459
|
function createSuperLineServer(contract, opts) {
|
|
16452
16460
|
const c = contract;
|
|
16453
16461
|
const serializer = opts.serializer ?? import_core2.jsonSerializer;
|
|
@@ -16456,6 +16464,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16456
16464
|
const inspectorRedact = new Set(
|
|
16457
16465
|
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
16458
16466
|
);
|
|
16467
|
+
const storeMap = opts.stores ?? {};
|
|
16459
16468
|
const conns = /* @__PURE__ */ new Set();
|
|
16460
16469
|
const inspectorConns = /* @__PURE__ */ new Set();
|
|
16461
16470
|
const members = /* @__PURE__ */ new Map();
|
|
@@ -16465,6 +16474,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16465
16474
|
const replyChannel = REPLY + instanceId;
|
|
16466
16475
|
let impl = {};
|
|
16467
16476
|
let closing = false;
|
|
16477
|
+
let relaying = false;
|
|
16468
16478
|
let nextSReq = 1;
|
|
16469
16479
|
const originWaiters = /* @__PURE__ */ new Map();
|
|
16470
16480
|
const ownerRouting = /* @__PURE__ */ new Map();
|
|
@@ -16497,6 +16507,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
16497
16507
|
handlePersonal(channel, payload);
|
|
16498
16508
|
return;
|
|
16499
16509
|
}
|
|
16510
|
+
if (channel.startsWith(STORE)) {
|
|
16511
|
+
handleStoreRelay(channel, payload);
|
|
16512
|
+
return;
|
|
16513
|
+
}
|
|
16500
16514
|
const set = members.get(channel);
|
|
16501
16515
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
16502
16516
|
const busSet = busListeners.get(channel);
|
|
@@ -16771,6 +16785,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
16771
16785
|
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
16772
16786
|
);
|
|
16773
16787
|
conn.transport = auth.transport;
|
|
16788
|
+
conn.principal = resolvePrincipal(conn, opts.identify);
|
|
16774
16789
|
raw.onMessage((bytes) => {
|
|
16775
16790
|
void onMessage(conn, bytes);
|
|
16776
16791
|
});
|
|
@@ -16820,7 +16835,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
16820
16835
|
else if (frame.t === "unsub") {
|
|
16821
16836
|
const ns = topicNamespace(conn.role, frame.c);
|
|
16822
16837
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
16823
|
-
} 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") {
|
|
16824
16843
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
16825
16844
|
} else if (frame.t === "serr") {
|
|
16826
16845
|
await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
|
|
@@ -16909,6 +16928,100 @@ function createSuperLineServer(contract, opts) {
|
|
|
16909
16928
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
16910
16929
|
});
|
|
16911
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
|
+
}
|
|
16912
17025
|
function room(name) {
|
|
16913
17026
|
const channel = ROOM + name;
|
|
16914
17027
|
return {
|
|
@@ -17013,6 +17126,45 @@ function createSuperLineServer(contract, opts) {
|
|
|
17013
17126
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
17014
17127
|
});
|
|
17015
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
|
+
}
|
|
17016
17168
|
const api = {
|
|
17017
17169
|
nodeId: instanceId,
|
|
17018
17170
|
nodeName,
|
|
@@ -17094,6 +17246,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
17094
17246
|
}
|
|
17095
17247
|
};
|
|
17096
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
|
+
},
|
|
17097
17254
|
async close() {
|
|
17098
17255
|
if (closing) return;
|
|
17099
17256
|
closing = true;
|
|
@@ -17112,5 +17269,6 @@ function createSuperLineServer(contract, opts) {
|
|
|
17112
17269
|
Conn,
|
|
17113
17270
|
MemoryBus,
|
|
17114
17271
|
createInMemoryAdapter,
|
|
17115
|
-
createSuperLineServer
|
|
17272
|
+
createSuperLineServer,
|
|
17273
|
+
resolvePrincipal
|
|
17116
17274
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport, Handshake } from '@super-line/core';
|
|
1
|
+
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, AccessRules, Resource, Perms, SharedTopics, EventData, ServerTransport, Handshake, ServerStore } from '@super-line/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A single client connection, passed to handlers as the third argument.
|
|
@@ -26,6 +26,8 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
|
|
|
26
26
|
data: Data;
|
|
27
27
|
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
28
28
|
transport?: string;
|
|
29
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
30
|
+
principal?: string;
|
|
29
31
|
/** When this connection was accepted (`Date.now()`). */
|
|
30
32
|
readonly connectedAt: number;
|
|
31
33
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
@@ -56,6 +58,12 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
|
|
|
56
58
|
/** Hard close with no handshake — used by heartbeat reaping. */
|
|
57
59
|
terminate(): void;
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The ACL principal for a connection: the stable `identify` key when configured, else the
|
|
63
|
+
* (random, per-connection) `conn.id`. Always returns a string — store access rules never key
|
|
64
|
+
* on `undefined`. Distinct from `identify`'s raw output (which may be undefined, used for presence).
|
|
65
|
+
*/
|
|
66
|
+
declare function resolvePrincipal(conn: Conn, identify?: (conn: Conn) => string | undefined): string;
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
69
|
* In-process pub/sub bus. Share one bus across multiple servers to simulate
|
|
@@ -210,6 +218,27 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
|
|
|
210
218
|
/** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
|
|
211
219
|
publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
|
|
212
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
|
|
223
|
+
* authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
|
|
224
|
+
* (stores are off-contract — see ADR-0003); callers assert the shape.
|
|
225
|
+
*/
|
|
226
|
+
interface ServerStoreHandle {
|
|
227
|
+
/** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
|
|
228
|
+
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
229
|
+
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
230
|
+
read(id: string): Promise<Resource | undefined>;
|
|
231
|
+
/** Server co-write: replace the Resource's value (LWW), fanned out to subscribers with a `server` origin. */
|
|
232
|
+
write(id: string, data: unknown): Promise<void>;
|
|
233
|
+
/** Grant a principal read/write on a Resource. */
|
|
234
|
+
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
235
|
+
/** Revoke a principal's access to a Resource entirely. */
|
|
236
|
+
revoke(id: string, principal: string): Promise<void>;
|
|
237
|
+
/** Delete a Resource. */
|
|
238
|
+
delete(id: string): Promise<void>;
|
|
239
|
+
/** All Resource ids in this store. */
|
|
240
|
+
list(): Promise<string[]>;
|
|
241
|
+
}
|
|
213
242
|
/** Options for {@link createSuperLineServer}. */
|
|
214
243
|
interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
|
|
215
244
|
/** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
|
|
@@ -251,6 +280,12 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
|
|
|
251
280
|
inspector?: boolean | {
|
|
252
281
|
redact?: string[];
|
|
253
282
|
};
|
|
283
|
+
/**
|
|
284
|
+
* Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
|
|
285
|
+
* Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
|
|
286
|
+
* `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
|
|
287
|
+
*/
|
|
288
|
+
stores?: Record<string, ServerStore>;
|
|
254
289
|
/** Called once per accepted connection. */
|
|
255
290
|
onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
|
|
256
291
|
/** Called when a connection closes, with the WebSocket close `code`. */
|
|
@@ -289,6 +324,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
|
|
|
289
324
|
subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
|
|
290
325
|
/** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
|
|
291
326
|
forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
|
|
327
|
+
/** Server-authoritative handle for a configured Store (`srv.store('scene').create(...)`). Throws `NOT_FOUND` if the name isn't configured. */
|
|
328
|
+
store(name: string): ServerStoreHandle;
|
|
292
329
|
close(): Promise<void>;
|
|
293
330
|
}
|
|
294
331
|
/**
|
|
@@ -315,4 +352,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
|
|
|
315
352
|
*/
|
|
316
353
|
declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
|
|
317
354
|
|
|
318
|
-
export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer };
|
|
355
|
+
export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerStoreHandle, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, ServerTransport, Handshake } from '@super-line/core';
|
|
1
|
+
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, SharedRequests, ServerInput, AnyData, RoleRequests, Events, DataOf, RoleTopics, AccessRules, Resource, Perms, SharedTopics, EventData, ServerTransport, Handshake, ServerStore } from '@super-line/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A single client connection, passed to handlers as the third argument.
|
|
@@ -26,6 +26,8 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
|
|
|
26
26
|
data: Data;
|
|
27
27
|
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
28
28
|
transport?: string;
|
|
29
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
30
|
+
principal?: string;
|
|
29
31
|
/** When this connection was accepted (`Date.now()`). */
|
|
30
32
|
readonly connectedAt: number;
|
|
31
33
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
@@ -56,6 +58,12 @@ declare class Conn<Ev = Record<string, ServerMessageDef>, Ctx = unknown, Role ex
|
|
|
56
58
|
/** Hard close with no handshake — used by heartbeat reaping. */
|
|
57
59
|
terminate(): void;
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The ACL principal for a connection: the stable `identify` key when configured, else the
|
|
63
|
+
* (random, per-connection) `conn.id`. Always returns a string — store access rules never key
|
|
64
|
+
* on `undefined`. Distinct from `identify`'s raw output (which may be undefined, used for presence).
|
|
65
|
+
*/
|
|
66
|
+
declare function resolvePrincipal(conn: Conn, identify?: (conn: Conn) => string | undefined): string;
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
69
|
* In-process pub/sub bus. Share one bus across multiple servers to simulate
|
|
@@ -210,6 +218,27 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
|
|
|
210
218
|
/** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
|
|
211
219
|
publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
|
|
212
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
|
|
223
|
+
* authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
|
|
224
|
+
* (stores are off-contract — see ADR-0003); callers assert the shape.
|
|
225
|
+
*/
|
|
226
|
+
interface ServerStoreHandle {
|
|
227
|
+
/** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
|
|
228
|
+
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
229
|
+
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
230
|
+
read(id: string): Promise<Resource | undefined>;
|
|
231
|
+
/** Server co-write: replace the Resource's value (LWW), fanned out to subscribers with a `server` origin. */
|
|
232
|
+
write(id: string, data: unknown): Promise<void>;
|
|
233
|
+
/** Grant a principal read/write on a Resource. */
|
|
234
|
+
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
235
|
+
/** Revoke a principal's access to a Resource entirely. */
|
|
236
|
+
revoke(id: string, principal: string): Promise<void>;
|
|
237
|
+
/** Delete a Resource. */
|
|
238
|
+
delete(id: string): Promise<void>;
|
|
239
|
+
/** All Resource ids in this store. */
|
|
240
|
+
list(): Promise<string[]>;
|
|
241
|
+
}
|
|
213
242
|
/** Options for {@link createSuperLineServer}. */
|
|
214
243
|
interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
|
|
215
244
|
/** Client↔server transports to accept connections on (e.g. `webSocketServerTransport({ server })`). */
|
|
@@ -251,6 +280,12 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>> {
|
|
|
251
280
|
inspector?: boolean | {
|
|
252
281
|
redact?: string[];
|
|
253
282
|
};
|
|
283
|
+
/**
|
|
284
|
+
* Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
|
|
285
|
+
* Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
|
|
286
|
+
* `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
|
|
287
|
+
*/
|
|
288
|
+
stores?: Record<string, ServerStore>;
|
|
254
289
|
/** Called once per accepted connection. */
|
|
255
290
|
onConnection?: (conn: Conn, ctx: CtxUnion<A>) => void;
|
|
256
291
|
/** Called when a connection closes, with the WebSocket close `code`. */
|
|
@@ -289,6 +324,8 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
|
|
|
289
324
|
subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
|
|
290
325
|
/** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
|
|
291
326
|
forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
|
|
327
|
+
/** Server-authoritative handle for a configured Store (`srv.store('scene').create(...)`). Throws `NOT_FOUND` if the name isn't configured. */
|
|
328
|
+
store(name: string): ServerStoreHandle;
|
|
292
329
|
close(): Promise<void>;
|
|
293
330
|
}
|
|
294
331
|
/**
|
|
@@ -315,4 +352,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>> {
|
|
|
315
352
|
*/
|
|
316
353
|
declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>>(contract: C, opts: SuperLineServerOptions<C, A>): SuperLineServer<C, A>;
|
|
317
354
|
|
|
318
|
-
export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer };
|
|
355
|
+
export { type AuthResult, type BusMeta, type ClusterView, Conn, type ConnTarget, type Handlers, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type RoleLens, type Room, type ServerStoreHandle, type SuperLineServer, type SuperLineServerOptions, type UserTarget, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,8 @@ var Conn = class {
|
|
|
32
32
|
data = {};
|
|
33
33
|
/** The client↔server transport (wire) this connection was accepted on (set by the server at accept). */
|
|
34
34
|
transport;
|
|
35
|
+
/** ACL identity for stores: `identify(conn) ?? conn.id`, set by the server at accept (always defined there). */
|
|
36
|
+
principal;
|
|
35
37
|
/** When this connection was accepted (`Date.now()`). */
|
|
36
38
|
connectedAt = Date.now();
|
|
37
39
|
/** When the server last sent a heartbeat ping to this connection (managed by the server). */
|
|
@@ -64,6 +66,9 @@ var Conn = class {
|
|
|
64
66
|
this.raw.terminate();
|
|
65
67
|
}
|
|
66
68
|
};
|
|
69
|
+
function resolvePrincipal(conn, identify) {
|
|
70
|
+
return identify?.(conn) ?? conn.id;
|
|
71
|
+
}
|
|
67
72
|
|
|
68
73
|
// src/memory-adapter.ts
|
|
69
74
|
var MemoryBus = class {
|
|
@@ -180,6 +185,8 @@ var CONN = "c:";
|
|
|
180
185
|
var USER = "u:";
|
|
181
186
|
var REPLY = "reply:";
|
|
182
187
|
var INSPECT = "i:";
|
|
188
|
+
var STORE = "s:";
|
|
189
|
+
var SERVER_ORIGIN = "server";
|
|
183
190
|
function createSuperLineServer(contract, opts) {
|
|
184
191
|
const c = contract;
|
|
185
192
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
@@ -188,6 +195,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
188
195
|
const inspectorRedact = new Set(
|
|
189
196
|
opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
|
|
190
197
|
);
|
|
198
|
+
const storeMap = opts.stores ?? {};
|
|
191
199
|
const conns = /* @__PURE__ */ new Set();
|
|
192
200
|
const inspectorConns = /* @__PURE__ */ new Set();
|
|
193
201
|
const members = /* @__PURE__ */ new Map();
|
|
@@ -197,6 +205,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
197
205
|
const replyChannel = REPLY + instanceId;
|
|
198
206
|
let impl = {};
|
|
199
207
|
let closing = false;
|
|
208
|
+
let relaying = false;
|
|
200
209
|
let nextSReq = 1;
|
|
201
210
|
const originWaiters = /* @__PURE__ */ new Map();
|
|
202
211
|
const ownerRouting = /* @__PURE__ */ new Map();
|
|
@@ -229,6 +238,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
229
238
|
handlePersonal(channel, payload);
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
241
|
+
if (channel.startsWith(STORE)) {
|
|
242
|
+
handleStoreRelay(channel, payload);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
232
245
|
const set = members.get(channel);
|
|
233
246
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
234
247
|
const busSet = busListeners.get(channel);
|
|
@@ -503,6 +516,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
503
516
|
inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
|
|
504
517
|
);
|
|
505
518
|
conn.transport = auth.transport;
|
|
519
|
+
conn.principal = resolvePrincipal(conn, opts.identify);
|
|
506
520
|
raw.onMessage((bytes) => {
|
|
507
521
|
void onMessage(conn, bytes);
|
|
508
522
|
});
|
|
@@ -552,7 +566,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
552
566
|
else if (frame.t === "unsub") {
|
|
553
567
|
const ns = topicNamespace(conn.role, frame.c);
|
|
554
568
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
555
|
-
} 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") {
|
|
556
574
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
557
575
|
} else if (frame.t === "serr") {
|
|
558
576
|
await handleClientReply(conn, frame.i, { ok: false, code: frame.code, m: frame.m, d: frame.d });
|
|
@@ -641,6 +659,100 @@ function createSuperLineServer(contract, opts) {
|
|
|
641
659
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
642
660
|
});
|
|
643
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
|
+
}
|
|
644
756
|
function room(name) {
|
|
645
757
|
const channel = ROOM + name;
|
|
646
758
|
return {
|
|
@@ -745,6 +857,45 @@ function createSuperLineServer(contract, opts) {
|
|
|
745
857
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
746
858
|
});
|
|
747
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
|
+
}
|
|
748
899
|
const api = {
|
|
749
900
|
nodeId: instanceId,
|
|
750
901
|
nodeName,
|
|
@@ -826,6 +977,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
826
977
|
}
|
|
827
978
|
};
|
|
828
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
|
+
},
|
|
829
985
|
async close() {
|
|
830
986
|
if (closing) return;
|
|
831
987
|
closing = true;
|
|
@@ -843,5 +999,6 @@ export {
|
|
|
843
999
|
Conn,
|
|
844
1000
|
MemoryBus,
|
|
845
1001
|
createInMemoryAdapter,
|
|
846
|
-
createSuperLineServer
|
|
1002
|
+
createSuperLineServer,
|
|
1003
|
+
resolvePrincipal
|
|
847
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,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@super-line/core": "^0.
|
|
50
|
+
"@super-line/core": "^0.5.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"zeromq": "^6.0.0",
|
|
69
69
|
"zod": "^3.24.1",
|
|
70
70
|
"zod-to-json-schema": "^3.25.2",
|
|
71
|
-
"@super-line/adapter-
|
|
72
|
-
"@super-line/adapter-
|
|
73
|
-
"@super-line/
|
|
74
|
-
"@super-line/transport-http": "0.
|
|
75
|
-
"@super-line/transport-loopback": "0.
|
|
76
|
-
"@super-line/
|
|
77
|
-
"@super-line/
|
|
78
|
-
"@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"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"@standard-community/standard-json": "^0.3.5"
|