@super-line/server 0.7.1 → 0.9.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 +52 -2
- package/dist/index.cjs +31 -12
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +31 -12
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @super-line/server
|
|
2
2
|
|
|
3
|
-
The server for [**super-line**](https://mertdogar.github.io/super-line/) —
|
|
3
|
+
The server for [**super-line**](https://mertdogar.github.io/super-line/) — the strictly-typed realtime data bus for TypeScript. Implements a shared contract over any transport: role-keyed request handlers, rooms, topics, synced state, middleware, lifecycle hooks, and node-to-node messaging. WebSocket is the default wire; HTTP/SSE, libp2p, and loopback are alternatives.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @super-line/core @super-line/server @super-line/transport-websocket zod
|
|
@@ -30,7 +30,57 @@ srv.implement({
|
|
|
30
30
|
server.listen(3000)
|
|
31
31
|
```
|
|
32
32
|
|
|
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;
|
|
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; HTTP/SSE ([`transport-http`](https://www.npmjs.com/package/@super-line/transport-http)), libp2p ([`transport-libp2p`](https://www.npmjs.com/package/@super-line/transport-libp2p)), and in-memory ([`transport-loopback`](https://www.npmjs.com/package/@super-line/transport-loopback)) are alternatives — see the Transports guide.
|
|
34
|
+
|
|
35
|
+
### Cluster event bus
|
|
36
|
+
|
|
37
|
+
`srv.publish(topic, data)` / `srv.subscribe(topic, handler)` is a server-side pub/sub over a shared contract topic. The callback fires for a publish from any node — including this one (local echo, in-process, no round-trip). `meta.from` is the publishing node; self-exclude with `if (meta.from === srv.nodeId) return`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const off = srv.subscribe('orders', (order, meta) => {
|
|
41
|
+
if (meta.from === srv.nodeId) return // skip our own echo
|
|
42
|
+
// react to an order published on another node
|
|
43
|
+
})
|
|
44
|
+
srv.publish('orders', { id: '...', total: 42 })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Cross-node fan-out (rooms, topics, the bus, targeted `toConn`/`toUser`, and store changes) rides a pluggable `adapter`. Defaults to an in-memory adapter (single process); for a cluster pass one of [`adapter-redis`](https://www.npmjs.com/package/@super-line/adapter-redis), [`adapter-libp2p`](https://www.npmjs.com/package/@super-line/adapter-libp2p), [`adapter-rabbitmq`](https://www.npmjs.com/package/@super-line/adapter-rabbitmq), or [`adapter-zeromq`](https://www.npmjs.com/package/@super-line/adapter-zeromq).
|
|
48
|
+
|
|
49
|
+
### Synced state (stores)
|
|
50
|
+
|
|
51
|
+
Pass server-side `stores` and the server becomes authoritative over persisted, reactive Resources — clients open the matching client halves. The store decides the consistency model (LWW or CRDT), durability (in-memory / SQLite / libsql / Postgres), and clustering (`relay` fans out over the adapter; `self` owns a central backend + per-node replica, no adapter). The default pair is [`@super-line/store-memory`](https://www.npmjs.com/package/@super-line/store-memory) (LWW · in-memory · relay).
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { memoryStoreServer } from '@super-line/store-memory'
|
|
55
|
+
|
|
56
|
+
const srv = createSuperLineServer(api, {
|
|
57
|
+
transports: [webSocketServerTransport({ server })],
|
|
58
|
+
authenticate,
|
|
59
|
+
stores: { docs: memoryStoreServer() },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const docs = srv.store('docs') // ServerStoreHandle — throws NOT_FOUND if unconfigured
|
|
63
|
+
await docs.create('intro', { title: 'Hi' }, { 'user:1': { read: true, write: true } })
|
|
64
|
+
await docs.grant('intro', 'user:2', { read: true })
|
|
65
|
+
await docs.write('intro', { title: 'Updated' }) // LWW co-write, fanned out with a `server` origin
|
|
66
|
+
await docs.delete('intro') // publishes a cluster-wide deletion (sdel) to every subscriber
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`ServerStoreHandle`: `create` / `read` / `write` / `grant` / `revoke` / `delete` / `list`. For a reactive in-process co-writer, `open(id)` returns a `ServerReplica` — the server half's mirror of the client's `store(ns).open(id)`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const replica = srv.store('docs').open('intro', { origin: 'agent' })
|
|
73
|
+
replica.subscribe(() => console.log(replica.getSnapshot()))
|
|
74
|
+
replica.update({ title: 'Live' }) // merge
|
|
75
|
+
replica.delete('title') // surgical key delete (the only way to remove a key server-side)
|
|
76
|
+
replica.close()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`open` requires a store with reactive support (throws `UNSUPPORTED` otherwise). CRDT stores (`store-sync`, `store-sync-libsql`, `store-sync-pglite`) and the LWW stores both back it.
|
|
80
|
+
|
|
81
|
+
### Control Center inspector
|
|
82
|
+
|
|
83
|
+
Set `inspector: true` (or `{ redact: ['password', 'token'] }`) to emit `msg.*` telemetry and accept read-only [Control Center](https://www.npmjs.com/package/@super-line/control-center) clients. The WS transport must also be created with `inspector: true` to negotiate the subprotocol. **Default off; dev / trusted-network only.**
|
|
34
84
|
|
|
35
85
|
- 📖 Docs: <https://mertdogar.github.io/super-line/>
|
|
36
86
|
- 📚 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
|
@@ -17023,20 +17023,29 @@ function createSuperLineServer(contract, opts) {
|
|
|
17023
17023
|
if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
17024
17024
|
}
|
|
17025
17025
|
for (const [name, store] of Object.entries(storeMap)) {
|
|
17026
|
+
const isSelf = store.clustering === "self";
|
|
17026
17027
|
store.onChange((change) => {
|
|
17028
|
+
const channel = STORE + name + ":" + change.id;
|
|
17029
|
+
if (isSelf) {
|
|
17030
|
+
const set = members.get(channel);
|
|
17031
|
+
if (!set) return;
|
|
17032
|
+
const payload = serializer.encode({ t: "sch", n: name, id: change.id, u: change.update, o: change.origin });
|
|
17033
|
+
for (const conn of set) conn.sendRaw(payload);
|
|
17034
|
+
return;
|
|
17035
|
+
}
|
|
17027
17036
|
if (relaying) return;
|
|
17028
17037
|
void adapter.publish(
|
|
17029
|
-
|
|
17030
|
-
serializer.encode({
|
|
17031
|
-
t: "sch",
|
|
17032
|
-
n: name,
|
|
17033
|
-
id: change.id,
|
|
17034
|
-
u: change.update,
|
|
17035
|
-
o: change.origin,
|
|
17036
|
-
nd: instanceId
|
|
17037
|
-
})
|
|
17038
|
+
channel,
|
|
17039
|
+
serializer.encode({ t: "sch", n: name, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
17038
17040
|
);
|
|
17039
17041
|
});
|
|
17042
|
+
if (isSelf)
|
|
17043
|
+
store.onDelete?.((id) => {
|
|
17044
|
+
const set = members.get(STORE + name + ":" + id);
|
|
17045
|
+
if (!set) return;
|
|
17046
|
+
const payload = serializer.encode({ t: "sdel", n: name, id });
|
|
17047
|
+
for (const conn of set) conn.sendRaw(payload);
|
|
17048
|
+
});
|
|
17040
17049
|
}
|
|
17041
17050
|
function handleStoreRelay(channel, payload) {
|
|
17042
17051
|
const set = members.get(channel);
|
|
@@ -17050,6 +17059,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
17050
17059
|
if (frame.nd === instanceId) return;
|
|
17051
17060
|
const store = storeMap[frame.n];
|
|
17052
17061
|
if (!store || store.clustering !== "relay") return;
|
|
17062
|
+
if (frame.t === "sdel") {
|
|
17063
|
+
void store.delete(frame.id);
|
|
17064
|
+
return;
|
|
17065
|
+
}
|
|
17053
17066
|
relaying = true;
|
|
17054
17067
|
try {
|
|
17055
17068
|
void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
@@ -17177,10 +17190,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
17177
17190
|
read(id) {
|
|
17178
17191
|
return Promise.resolve(store.read(id));
|
|
17179
17192
|
},
|
|
17180
|
-
async write(id, data) {
|
|
17181
|
-
|
|
17193
|
+
async write(id, data, opts2) {
|
|
17194
|
+
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
17195
|
+
await store.apply({ id, update: data, origin });
|
|
17182
17196
|
if (inspectorEnabled)
|
|
17183
|
-
emitInspectorEvent({ type: "store.write", store: name, id, origin
|
|
17197
|
+
emitInspectorEvent({ type: "store.write", store: name, id, origin, data: safeSnapshot(data) });
|
|
17184
17198
|
},
|
|
17185
17199
|
async grant(id, principal, perms) {
|
|
17186
17200
|
const r = await readOrThrow(id);
|
|
@@ -17196,6 +17210,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
17196
17210
|
},
|
|
17197
17211
|
async delete(id) {
|
|
17198
17212
|
await store.delete(id);
|
|
17213
|
+
if (store.clustering !== "self")
|
|
17214
|
+
void adapter.publish(
|
|
17215
|
+
STORE + name + ":" + id,
|
|
17216
|
+
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
17217
|
+
);
|
|
17199
17218
|
if (inspectorEnabled) emitInspectorEvent({ type: "store.delete", store: name, id });
|
|
17200
17219
|
},
|
|
17201
17220
|
list() {
|
package/dist/index.d.cts
CHANGED
|
@@ -228,8 +228,13 @@ interface ServerStoreHandle {
|
|
|
228
228
|
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
229
229
|
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
230
230
|
read(id: string): Promise<Resource | undefined>;
|
|
231
|
-
/**
|
|
232
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
|
|
233
|
+
* (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
|
|
234
|
+
*/
|
|
235
|
+
write(id: string, data: unknown, opts?: {
|
|
236
|
+
origin?: string;
|
|
237
|
+
}): Promise<void>;
|
|
233
238
|
/** Grant a principal read/write on a Resource. */
|
|
234
239
|
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
235
240
|
/** Revoke a principal's access to a Resource entirely. */
|
package/dist/index.d.ts
CHANGED
|
@@ -228,8 +228,13 @@ interface ServerStoreHandle {
|
|
|
228
228
|
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
229
229
|
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
230
230
|
read(id: string): Promise<Resource | undefined>;
|
|
231
|
-
/**
|
|
232
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
|
|
233
|
+
* (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
|
|
234
|
+
*/
|
|
235
|
+
write(id: string, data: unknown, opts?: {
|
|
236
|
+
origin?: string;
|
|
237
|
+
}): Promise<void>;
|
|
233
238
|
/** Grant a principal read/write on a Resource. */
|
|
234
239
|
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
235
240
|
/** Revoke a principal's access to a Resource entirely. */
|
package/dist/index.js
CHANGED
|
@@ -755,20 +755,29 @@ function createSuperLineServer(contract, opts) {
|
|
|
755
755
|
if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
756
756
|
}
|
|
757
757
|
for (const [name, store] of Object.entries(storeMap)) {
|
|
758
|
+
const isSelf = store.clustering === "self";
|
|
758
759
|
store.onChange((change) => {
|
|
760
|
+
const channel = STORE + name + ":" + change.id;
|
|
761
|
+
if (isSelf) {
|
|
762
|
+
const set = members.get(channel);
|
|
763
|
+
if (!set) return;
|
|
764
|
+
const payload = serializer.encode({ t: "sch", n: name, id: change.id, u: change.update, o: change.origin });
|
|
765
|
+
for (const conn of set) conn.sendRaw(payload);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
759
768
|
if (relaying) return;
|
|
760
769
|
void adapter.publish(
|
|
761
|
-
|
|
762
|
-
serializer.encode({
|
|
763
|
-
t: "sch",
|
|
764
|
-
n: name,
|
|
765
|
-
id: change.id,
|
|
766
|
-
u: change.update,
|
|
767
|
-
o: change.origin,
|
|
768
|
-
nd: instanceId
|
|
769
|
-
})
|
|
770
|
+
channel,
|
|
771
|
+
serializer.encode({ t: "sch", n: name, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
770
772
|
);
|
|
771
773
|
});
|
|
774
|
+
if (isSelf)
|
|
775
|
+
store.onDelete?.((id) => {
|
|
776
|
+
const set = members.get(STORE + name + ":" + id);
|
|
777
|
+
if (!set) return;
|
|
778
|
+
const payload = serializer.encode({ t: "sdel", n: name, id });
|
|
779
|
+
for (const conn of set) conn.sendRaw(payload);
|
|
780
|
+
});
|
|
772
781
|
}
|
|
773
782
|
function handleStoreRelay(channel, payload) {
|
|
774
783
|
const set = members.get(channel);
|
|
@@ -782,6 +791,10 @@ function createSuperLineServer(contract, opts) {
|
|
|
782
791
|
if (frame.nd === instanceId) return;
|
|
783
792
|
const store = storeMap[frame.n];
|
|
784
793
|
if (!store || store.clustering !== "relay") return;
|
|
794
|
+
if (frame.t === "sdel") {
|
|
795
|
+
void store.delete(frame.id);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
785
798
|
relaying = true;
|
|
786
799
|
try {
|
|
787
800
|
void store.apply({ id: frame.id, update: frame.u, origin: frame.o });
|
|
@@ -909,10 +922,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
909
922
|
read(id) {
|
|
910
923
|
return Promise.resolve(store.read(id));
|
|
911
924
|
},
|
|
912
|
-
async write(id, data) {
|
|
913
|
-
|
|
925
|
+
async write(id, data, opts2) {
|
|
926
|
+
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
927
|
+
await store.apply({ id, update: data, origin });
|
|
914
928
|
if (inspectorEnabled)
|
|
915
|
-
emitInspectorEvent({ type: "store.write", store: name, id, origin
|
|
929
|
+
emitInspectorEvent({ type: "store.write", store: name, id, origin, data: safeSnapshot(data) });
|
|
916
930
|
},
|
|
917
931
|
async grant(id, principal, perms) {
|
|
918
932
|
const r = await readOrThrow(id);
|
|
@@ -928,6 +942,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
928
942
|
},
|
|
929
943
|
async delete(id) {
|
|
930
944
|
await store.delete(id);
|
|
945
|
+
if (store.clustering !== "self")
|
|
946
|
+
void adapter.publish(
|
|
947
|
+
STORE + name + ":" + id,
|
|
948
|
+
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
949
|
+
);
|
|
931
950
|
if (inspectorEnabled) emitInspectorEvent({ type: "store.delete", store: name, id });
|
|
932
951
|
},
|
|
933
952
|
list() {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-line/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Realtime data-bus server for super-line — requests, events, rooms, topics, stores, pluggable transports & adapters.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Mert",
|
|
8
8
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@super-line/core": "^0.
|
|
50
|
+
"@super-line/core": "^0.8.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
@@ -69,13 +69,13 @@
|
|
|
69
69
|
"zod": "^3.24.1",
|
|
70
70
|
"zod-to-json-schema": "^3.25.2",
|
|
71
71
|
"@super-line/adapter-libp2p": "0.5.0",
|
|
72
|
-
"@super-line/adapter-redis": "0.5.0",
|
|
73
72
|
"@super-line/adapter-rabbitmq": "0.5.0",
|
|
74
|
-
"@super-line/
|
|
75
|
-
"@super-line/
|
|
76
|
-
"@super-line/
|
|
73
|
+
"@super-line/client": "0.7.0",
|
|
74
|
+
"@super-line/adapter-zeromq": "0.5.0",
|
|
75
|
+
"@super-line/adapter-redis": "0.5.0",
|
|
77
76
|
"@super-line/transport-http": "0.5.0",
|
|
78
|
-
"@super-line/
|
|
77
|
+
"@super-line/transport-websocket": "0.5.0",
|
|
78
|
+
"@super-line/transport-loopback": "0.5.0"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"@standard-community/standard-json": "^0.3.5"
|