@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @super-line/server
2
2
 
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.
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; 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
+ 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
- STORE + name + ":" + change.id,
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
- await store.apply({ id, update: data, origin: SERVER_ORIGIN });
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: SERVER_ORIGIN, data: safeSnapshot(data) });
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
- /** 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>;
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
- /** 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>;
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
- STORE + name + ":" + change.id,
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
- await store.apply({ id, update: data, origin: SERVER_ORIGIN });
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: SERVER_ORIGIN, data: safeSnapshot(data) });
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.7.1",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
- "description": "Typesafe WebSocket server for super-line — rooms, topics, middleware, pluggable adapters.",
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.7.1"
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/transport-websocket": "0.5.0",
75
- "@super-line/transport-loopback": "0.5.0",
76
- "@super-line/client": "0.6.0",
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/adapter-zeromq": "0.5.0"
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"