@super-line/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @super-line/core
2
+
3
+ Shared core for [**super-line**](https://mertdogar.github.io/super-line/) — end-to-end typesafe WebSockets for TypeScript. This package holds the pieces both ends import: `defineContract`, runtime validation, the `SocketError` model, and the `Serializer` / `Adapter` interfaces.
4
+
5
+ ```bash
6
+ pnpm add @super-line/core zod
7
+ ```
8
+
9
+ ```ts
10
+ import { z } from 'zod'
11
+ import { defineContract } from '@super-line/core'
12
+
13
+ export const api = defineContract({
14
+ shared: {
15
+ serverToClient: { message: { payload: z.object({ text: z.string() }) } },
16
+ },
17
+ roles: {
18
+ user: {
19
+ clientToServer: {
20
+ send: { input: z.object({ text: z.string() }), output: z.object({ id: z.string() }) },
21
+ },
22
+ },
23
+ },
24
+ })
25
+ ```
26
+
27
+ The contract is split by **direction** (`clientToServer` / `serverToClient`) and scoped by **role**, then implemented by [`@super-line/server`](https://www.npmjs.com/package/@super-line/server) and called by [`@super-line/client`](https://www.npmjs.com/package/@super-line/client).
28
+
29
+ - 📖 Docs: <https://mertdogar.github.io/super-line/>
30
+ - 📚 The contract model: <https://mertdogar.github.io/super-line/guide/the-contract>
31
+ - 🧩 Source: <https://github.com/mertdogar/super-line>
32
+
33
+ MIT © Mert
package/dist/index.cjs ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PROTOCOL: () => PROTOCOL,
24
+ SocketError: () => SocketError,
25
+ defineContract: () => defineContract,
26
+ jsonSerializer: () => jsonSerializer,
27
+ validate: () => validate,
28
+ validateSync: () => validateSync
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/errors.ts
33
+ var SocketError = class extends Error {
34
+ /** The typed error code (e.g. `'FORBIDDEN'`), available on the client. */
35
+ code;
36
+ /** Optional structured data attached to the error, delivered to the client. */
37
+ data;
38
+ /**
39
+ * @param code - a {@link SocketErrorCode} or custom string.
40
+ * @param message - human-readable message (defaults to `code`).
41
+ * @param data - optional structured payload delivered to the client.
42
+ */
43
+ constructor(code, message, data) {
44
+ super(message ?? code);
45
+ this.name = "SocketError";
46
+ this.code = code;
47
+ this.data = data;
48
+ }
49
+ };
50
+
51
+ // src/serializer.ts
52
+ var decoder = new TextDecoder();
53
+ var jsonSerializer = {
54
+ encode: (value) => JSON.stringify(value),
55
+ decode: (data) => JSON.parse(typeof data === "string" ? data : decoder.decode(data))
56
+ };
57
+
58
+ // src/contract.ts
59
+ function defineContract(contract) {
60
+ return contract;
61
+ }
62
+ async function validate(schema, value) {
63
+ let result = schema["~standard"].validate(value);
64
+ if (result instanceof Promise) result = await result;
65
+ if (result.issues) {
66
+ throw new SocketError("VALIDATION", "Validation failed", result.issues);
67
+ }
68
+ return result.value;
69
+ }
70
+ function validateSync(schema, value) {
71
+ const result = schema["~standard"].validate(value);
72
+ if (result instanceof Promise) {
73
+ throw new SocketError("INTERNAL", "Async schema not supported for synchronous validation");
74
+ }
75
+ if (result.issues) {
76
+ throw new SocketError("VALIDATION", "Validation failed", result.issues);
77
+ }
78
+ return result.value;
79
+ }
80
+
81
+ // src/wire.ts
82
+ var PROTOCOL = "superline.v1";
83
+ // Annotate the CommonJS export names for ESM import in node:
84
+ 0 && (module.exports = {
85
+ PROTOCOL,
86
+ SocketError,
87
+ defineContract,
88
+ jsonSerializer,
89
+ validate,
90
+ validateSync
91
+ });
@@ -0,0 +1,248 @@
1
+ import { StandardSchemaV1 } from '@standard-schema/spec';
2
+
3
+ /** The built-in error codes super-line uses across the wire. */
4
+ type SocketErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'TIMEOUT' | 'VALIDATION' | 'DISCONNECTED' | 'INTERNAL';
5
+ /** A built-in code or any custom string (autocomplete keeps the known set). */
6
+ type ErrorCode = SocketErrorCode | (string & {});
7
+ /**
8
+ * The error type carried end-to-end. Throw one from a handler and the client's
9
+ * promise rejects with the same `code` (and optional `data`). Unknown throws
10
+ * become `INTERNAL` so server internals aren't leaked.
11
+ */
12
+ declare class SocketError<Data = unknown> extends Error {
13
+ /** The typed error code (e.g. `'FORBIDDEN'`), available on the client. */
14
+ readonly code: ErrorCode;
15
+ /** Optional structured data attached to the error, delivered to the client. */
16
+ readonly data?: Data;
17
+ /**
18
+ * @param code - a {@link SocketErrorCode} or custom string.
19
+ * @param message - human-readable message (defaults to `code`).
20
+ * @param data - optional structured payload delivered to the client.
21
+ */
22
+ constructor(code: ErrorCode, message?: string, data?: Data);
23
+ }
24
+
25
+ /**
26
+ * Wire encoder/decoder. The server and client **must** use the same one.
27
+ * Swap in `superjson`/msgpack to carry richer types than JSON.
28
+ */
29
+ interface Serializer {
30
+ /** Encode a frame for the wire. */
31
+ encode(value: unknown): string | Uint8Array;
32
+ /** Decode a wire frame back to a value. */
33
+ decode(data: string | Uint8Array): unknown;
34
+ }
35
+ /** The default serializer (`JSON`). Note: turns `Date` into a string — see the serialization guide. */
36
+ declare const jsonSerializer: Serializer;
37
+
38
+ /**
39
+ * Cross-node fan-out seam. Rooms, topics, and serverToServer all compile down to
40
+ * channel pub/sub. A node subscribes to a channel only while it has a local member,
41
+ * and publishes always go through the adapter (the in-memory adapter loops back),
42
+ * so a node delivers to its local members on receipt — one code path, no double-send.
43
+ *
44
+ * The default is a per-server in-memory adapter; use `@super-line/adapter-redis`
45
+ * to fan out across processes.
46
+ */
47
+ interface Adapter {
48
+ /** Start receiving messages published to `channel`. */
49
+ subscribe(channel: string): void | Promise<void>;
50
+ /** Stop receiving messages for `channel`. */
51
+ unsubscribe(channel: string): void | Promise<void>;
52
+ /** Publish an encoded payload to `channel` (delivered to every subscribed node). */
53
+ publish(channel: string, payload: string | Uint8Array): void | Promise<void>;
54
+ /** Register the handler invoked for each message on a subscribed channel. */
55
+ onMessage(handler: (channel: string, payload: string | Uint8Array) => void): void;
56
+ /** Optional teardown (e.g. close Redis connections). */
57
+ close?(): void | Promise<void>;
58
+ }
59
+
60
+ /** Any [Standard Schema](https://standardschema.dev) validator (Zod, Valibot, ArkType…). */
61
+ type Schema = StandardSchemaV1;
62
+ /**
63
+ * A client→server request (request/response). The client sends `input`; the
64
+ * server validates it, runs the handler, and replies with `output`.
65
+ * Fire-and-forget signals (no `output`) are not supported yet.
66
+ */
67
+ interface RequestDef {
68
+ /** Schema for the request payload the client sends. */
69
+ input: Schema;
70
+ /** Schema for the reply the server returns. */
71
+ output: Schema;
72
+ }
73
+ /**
74
+ * A server→client message. With `subscribe: true` it becomes a client-subscribable
75
+ * **topic**; otherwise it is a server-pushed **event**.
76
+ */
77
+ interface ServerMessageDef {
78
+ /** Schema for the message body. */
79
+ payload: Schema;
80
+ /** When `true`, clients opt in via `client.subscribe(...)` (a topic). Omit for a push event. */
81
+ subscribe?: boolean;
82
+ }
83
+ /** The two directions within a `shared` or role block. */
84
+ interface Directional {
85
+ /** Requests this side may call (client→server). */
86
+ clientToServer?: Record<string, RequestDef>;
87
+ /** Events/topics this side may receive (server→client). */
88
+ serverToClient?: Record<string, ServerMessageDef>;
89
+ }
90
+ /**
91
+ * The single source of truth, imported by both server and client. Split by
92
+ * **direction** and scoped by **role**: a `shared` base every role inherits,
93
+ * plus one block per role. `serverToServer` is node↔node (not role-scoped).
94
+ */
95
+ interface Contract {
96
+ /** Surface common to every role (merged into each role's effective surface). */
97
+ shared?: Directional;
98
+ /** Per-role surfaces. A connection's role selects which one (plus `shared`) it sees. */
99
+ roles: Record<string, Directional>;
100
+ /** Typed node-to-node event payloads, for {@link "@super-line/server"!}'s `emitServer`/`onServer`. */
101
+ serverToServer?: Record<string, Schema>;
102
+ }
103
+ /**
104
+ * Define a contract. An identity function — `const` preserves literal keys and
105
+ * `subscribe: true` so the full surface can be inferred on both ends.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { z } from 'zod'
110
+ * import { defineContract } from '@super-line/core'
111
+ *
112
+ * export const api = defineContract({
113
+ * shared: {
114
+ * clientToServer: { join: { input: z.object({ room: z.string() }), output: z.object({ ok: z.boolean() }) } },
115
+ * serverToClient: { message: { payload: z.object({ text: z.string() }) } },
116
+ * },
117
+ * roles: {
118
+ * user: { clientToServer: { say: { input: z.object({ text: z.string() }), output: z.object({ id: z.string() }) } } },
119
+ * agent: { clientToServer: { announce: { input: z.object({ text: z.string() }), output: z.object({ id: z.string() }) } } },
120
+ * },
121
+ * serverToServer: { rebalance: z.object({ shard: z.number() }) },
122
+ * })
123
+ * ```
124
+ */
125
+ declare function defineContract<const C extends Contract>(contract: C): C;
126
+ /** Union of a contract's role names. */
127
+ type RoleOf<C extends Contract> = keyof C['roles'] & string;
128
+ type CtsOf<D> = D extends {
129
+ clientToServer: infer M extends Record<string, RequestDef>;
130
+ } ? M : {};
131
+ type StcOf<D> = D extends {
132
+ serverToClient: infer M extends Record<string, ServerMessageDef>;
133
+ } ? M : {};
134
+ type EventsOf<M> = {
135
+ [K in keyof M as M[K] extends {
136
+ subscribe: true;
137
+ } ? never : K]: M[K];
138
+ };
139
+ type TopicsOf<M> = {
140
+ [K in keyof M as M[K] extends {
141
+ subscribe: true;
142
+ } ? K : never]: M[K];
143
+ };
144
+ /** A role's effective request map: `shared` ∪ `roles[R]` client→server requests. */
145
+ type Requests<C extends Contract, R extends RoleOf<C>> = CtsOf<C['shared']> & CtsOf<C['roles'][R]>;
146
+ /** A role's effective server→client map (events and topics combined). */
147
+ type ServerMessages<C extends Contract, R extends RoleOf<C>> = StcOf<C['shared']> & StcOf<C['roles'][R]>;
148
+ /** A role's push events (server→client entries without `subscribe`). */
149
+ type Events<C extends Contract, R extends RoleOf<C>> = EventsOf<ServerMessages<C, R>>;
150
+ /** A role's subscribable topics (server→client entries with `subscribe: true`). */
151
+ type Topics<C extends Contract, R extends RoleOf<C>> = TopicsOf<ServerMessages<C, R>>;
152
+ /** Requests in the `shared` block (every role can call these). */
153
+ type SharedRequests<C extends Contract> = CtsOf<C['shared']>;
154
+ /** Requests in one role's block (not including `shared`). */
155
+ type RoleRequests<C extends Contract, R extends RoleOf<C>> = CtsOf<C['roles'][R]>;
156
+ /** Push events in the `shared` block (broadcastable to a mixed-role room). */
157
+ type SharedEvents<C extends Contract> = EventsOf<StcOf<C['shared']>>;
158
+ /** Subscribable topics in the `shared` block (published via `srv.publish`). */
159
+ type SharedTopics<C extends Contract> = TopicsOf<StcOf<C['shared']>>;
160
+ /** Subscribable topics in one role's block (published via `srv.forRole(r).publish`). */
161
+ type RoleTopics<C extends Contract, R extends RoleOf<C>> = TopicsOf<StcOf<C['roles'][R]>>;
162
+ /** The `serverToServer` map, or `{}` if the contract has none. */
163
+ type ServerEvents<C extends Contract> = C['serverToServer'] extends Record<string, Schema> ? C['serverToServer'] : {};
164
+ /** The input type a client passes for a request (pre-validation). */
165
+ type ClientInput<T> = T extends RequestDef ? InferIn<T['input']> : never;
166
+ /** The input type a server handler receives for a request (post-validation). */
167
+ type ServerInput<T> = T extends RequestDef ? InferOut<T['input']> : never;
168
+ /** The reply type of a request (server returns / client receives). */
169
+ type Output<T> = T extends RequestDef ? InferOut<T['output']> : never;
170
+ /** The data a client receives for an event/topic (post-validation). */
171
+ type EventData<T> = T extends ServerMessageDef ? InferOut<T['payload']> : never;
172
+ /** The data a server sends for an event/topic (pre-validation). */
173
+ type EmitData<T> = T extends ServerMessageDef ? InferIn<T['payload']> : never;
174
+ /** The data a server sends for a serverToServer event. */
175
+ type ServerEmit<T> = T extends Schema ? InferIn<T> : never;
176
+ /** The data a server receives for a serverToServer event. */
177
+ type ServerData<T> = T extends Schema ? InferOut<T> : never;
178
+ /** Infer a schema's **input** type (what you pass into the validator). */
179
+ type InferIn<S extends Schema> = StandardSchemaV1.InferInput<S>;
180
+ /** Infer a schema's **output** type (the validated result). */
181
+ type InferOut<S extends Schema> = StandardSchemaV1.InferOutput<S>;
182
+ /**
183
+ * Validate a value against a Standard Schema validator (sync or async).
184
+ *
185
+ * @param schema - the validator to run.
186
+ * @param value - the untrusted value to validate.
187
+ * @returns the parsed, typed value.
188
+ * @throws {@link SocketError} with code `VALIDATION` if the value doesn't match.
189
+ */
190
+ declare function validate<S extends Schema>(schema: S, value: unknown): Promise<StandardSchemaV1.InferOutput<S>>;
191
+ /**
192
+ * Synchronous validation for hot paths (e.g. client inbound dispatch).
193
+ *
194
+ * @param schema - the validator to run.
195
+ * @param value - the untrusted value to validate.
196
+ * @returns the parsed, typed value.
197
+ * @throws {@link SocketError} with code `VALIDATION` on mismatch, or `INTERNAL` if the schema is async.
198
+ */
199
+ declare function validateSync<S extends Schema>(schema: S, value: unknown): StandardSchemaV1.InferOutput<S>;
200
+
201
+ /**
202
+ * The wire protocol below is an implementation detail — you rarely touch frames
203
+ * directly. It's exported for adapters, custom transports, and tooling.
204
+ */
205
+ /** Protocol version string, negotiated via the WebSocket subprotocol at upgrade. */
206
+ declare const PROTOCOL = "superline.v1";
207
+ interface ReqFrame {
208
+ t: 'req';
209
+ i: number;
210
+ m: string;
211
+ d: unknown;
212
+ }
213
+ interface SubFrame {
214
+ t: 'sub';
215
+ i: number;
216
+ c: string;
217
+ }
218
+ interface UnsubFrame {
219
+ t: 'unsub';
220
+ c: string;
221
+ }
222
+ type ClientFrame = ReqFrame | SubFrame | UnsubFrame;
223
+ interface ResFrame {
224
+ t: 'res';
225
+ i: number;
226
+ d: unknown;
227
+ }
228
+ interface ErrFrame {
229
+ t: 'err';
230
+ i?: number;
231
+ code: string;
232
+ m: string;
233
+ d?: unknown;
234
+ }
235
+ interface EvtFrame {
236
+ t: 'evt';
237
+ e: string;
238
+ d: unknown;
239
+ }
240
+ interface PubFrame {
241
+ t: 'pub';
242
+ c: string;
243
+ d: unknown;
244
+ }
245
+ type ServerFrame = ResFrame | ErrFrame | EvtFrame | PubFrame;
246
+ type Frame = ClientFrame | ServerFrame;
247
+
248
+ export { type Adapter, type ClientFrame, type ClientInput, type Contract, type Directional, type EmitData, type ErrFrame, type ErrorCode, type EventData, type Events, type EvtFrame, type Frame, type InferIn, type InferOut, type Output, PROTOCOL, type PubFrame, type ReqFrame, type RequestDef, type Requests, type ResFrame, type RoleOf, type RoleRequests, type RoleTopics, type Schema, type Serializer, type ServerData, type ServerEmit, type ServerEvents, type ServerFrame, type ServerInput, type ServerMessageDef, type ServerMessages, type SharedEvents, type SharedRequests, type SharedTopics, SocketError, type SocketErrorCode, type SubFrame, type Topics, type UnsubFrame, defineContract, jsonSerializer, validate, validateSync };
@@ -0,0 +1,248 @@
1
+ import { StandardSchemaV1 } from '@standard-schema/spec';
2
+
3
+ /** The built-in error codes super-line uses across the wire. */
4
+ type SocketErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'TIMEOUT' | 'VALIDATION' | 'DISCONNECTED' | 'INTERNAL';
5
+ /** A built-in code or any custom string (autocomplete keeps the known set). */
6
+ type ErrorCode = SocketErrorCode | (string & {});
7
+ /**
8
+ * The error type carried end-to-end. Throw one from a handler and the client's
9
+ * promise rejects with the same `code` (and optional `data`). Unknown throws
10
+ * become `INTERNAL` so server internals aren't leaked.
11
+ */
12
+ declare class SocketError<Data = unknown> extends Error {
13
+ /** The typed error code (e.g. `'FORBIDDEN'`), available on the client. */
14
+ readonly code: ErrorCode;
15
+ /** Optional structured data attached to the error, delivered to the client. */
16
+ readonly data?: Data;
17
+ /**
18
+ * @param code - a {@link SocketErrorCode} or custom string.
19
+ * @param message - human-readable message (defaults to `code`).
20
+ * @param data - optional structured payload delivered to the client.
21
+ */
22
+ constructor(code: ErrorCode, message?: string, data?: Data);
23
+ }
24
+
25
+ /**
26
+ * Wire encoder/decoder. The server and client **must** use the same one.
27
+ * Swap in `superjson`/msgpack to carry richer types than JSON.
28
+ */
29
+ interface Serializer {
30
+ /** Encode a frame for the wire. */
31
+ encode(value: unknown): string | Uint8Array;
32
+ /** Decode a wire frame back to a value. */
33
+ decode(data: string | Uint8Array): unknown;
34
+ }
35
+ /** The default serializer (`JSON`). Note: turns `Date` into a string — see the serialization guide. */
36
+ declare const jsonSerializer: Serializer;
37
+
38
+ /**
39
+ * Cross-node fan-out seam. Rooms, topics, and serverToServer all compile down to
40
+ * channel pub/sub. A node subscribes to a channel only while it has a local member,
41
+ * and publishes always go through the adapter (the in-memory adapter loops back),
42
+ * so a node delivers to its local members on receipt — one code path, no double-send.
43
+ *
44
+ * The default is a per-server in-memory adapter; use `@super-line/adapter-redis`
45
+ * to fan out across processes.
46
+ */
47
+ interface Adapter {
48
+ /** Start receiving messages published to `channel`. */
49
+ subscribe(channel: string): void | Promise<void>;
50
+ /** Stop receiving messages for `channel`. */
51
+ unsubscribe(channel: string): void | Promise<void>;
52
+ /** Publish an encoded payload to `channel` (delivered to every subscribed node). */
53
+ publish(channel: string, payload: string | Uint8Array): void | Promise<void>;
54
+ /** Register the handler invoked for each message on a subscribed channel. */
55
+ onMessage(handler: (channel: string, payload: string | Uint8Array) => void): void;
56
+ /** Optional teardown (e.g. close Redis connections). */
57
+ close?(): void | Promise<void>;
58
+ }
59
+
60
+ /** Any [Standard Schema](https://standardschema.dev) validator (Zod, Valibot, ArkType…). */
61
+ type Schema = StandardSchemaV1;
62
+ /**
63
+ * A client→server request (request/response). The client sends `input`; the
64
+ * server validates it, runs the handler, and replies with `output`.
65
+ * Fire-and-forget signals (no `output`) are not supported yet.
66
+ */
67
+ interface RequestDef {
68
+ /** Schema for the request payload the client sends. */
69
+ input: Schema;
70
+ /** Schema for the reply the server returns. */
71
+ output: Schema;
72
+ }
73
+ /**
74
+ * A server→client message. With `subscribe: true` it becomes a client-subscribable
75
+ * **topic**; otherwise it is a server-pushed **event**.
76
+ */
77
+ interface ServerMessageDef {
78
+ /** Schema for the message body. */
79
+ payload: Schema;
80
+ /** When `true`, clients opt in via `client.subscribe(...)` (a topic). Omit for a push event. */
81
+ subscribe?: boolean;
82
+ }
83
+ /** The two directions within a `shared` or role block. */
84
+ interface Directional {
85
+ /** Requests this side may call (client→server). */
86
+ clientToServer?: Record<string, RequestDef>;
87
+ /** Events/topics this side may receive (server→client). */
88
+ serverToClient?: Record<string, ServerMessageDef>;
89
+ }
90
+ /**
91
+ * The single source of truth, imported by both server and client. Split by
92
+ * **direction** and scoped by **role**: a `shared` base every role inherits,
93
+ * plus one block per role. `serverToServer` is node↔node (not role-scoped).
94
+ */
95
+ interface Contract {
96
+ /** Surface common to every role (merged into each role's effective surface). */
97
+ shared?: Directional;
98
+ /** Per-role surfaces. A connection's role selects which one (plus `shared`) it sees. */
99
+ roles: Record<string, Directional>;
100
+ /** Typed node-to-node event payloads, for {@link "@super-line/server"!}'s `emitServer`/`onServer`. */
101
+ serverToServer?: Record<string, Schema>;
102
+ }
103
+ /**
104
+ * Define a contract. An identity function — `const` preserves literal keys and
105
+ * `subscribe: true` so the full surface can be inferred on both ends.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { z } from 'zod'
110
+ * import { defineContract } from '@super-line/core'
111
+ *
112
+ * export const api = defineContract({
113
+ * shared: {
114
+ * clientToServer: { join: { input: z.object({ room: z.string() }), output: z.object({ ok: z.boolean() }) } },
115
+ * serverToClient: { message: { payload: z.object({ text: z.string() }) } },
116
+ * },
117
+ * roles: {
118
+ * user: { clientToServer: { say: { input: z.object({ text: z.string() }), output: z.object({ id: z.string() }) } } },
119
+ * agent: { clientToServer: { announce: { input: z.object({ text: z.string() }), output: z.object({ id: z.string() }) } } },
120
+ * },
121
+ * serverToServer: { rebalance: z.object({ shard: z.number() }) },
122
+ * })
123
+ * ```
124
+ */
125
+ declare function defineContract<const C extends Contract>(contract: C): C;
126
+ /** Union of a contract's role names. */
127
+ type RoleOf<C extends Contract> = keyof C['roles'] & string;
128
+ type CtsOf<D> = D extends {
129
+ clientToServer: infer M extends Record<string, RequestDef>;
130
+ } ? M : {};
131
+ type StcOf<D> = D extends {
132
+ serverToClient: infer M extends Record<string, ServerMessageDef>;
133
+ } ? M : {};
134
+ type EventsOf<M> = {
135
+ [K in keyof M as M[K] extends {
136
+ subscribe: true;
137
+ } ? never : K]: M[K];
138
+ };
139
+ type TopicsOf<M> = {
140
+ [K in keyof M as M[K] extends {
141
+ subscribe: true;
142
+ } ? K : never]: M[K];
143
+ };
144
+ /** A role's effective request map: `shared` ∪ `roles[R]` client→server requests. */
145
+ type Requests<C extends Contract, R extends RoleOf<C>> = CtsOf<C['shared']> & CtsOf<C['roles'][R]>;
146
+ /** A role's effective server→client map (events and topics combined). */
147
+ type ServerMessages<C extends Contract, R extends RoleOf<C>> = StcOf<C['shared']> & StcOf<C['roles'][R]>;
148
+ /** A role's push events (server→client entries without `subscribe`). */
149
+ type Events<C extends Contract, R extends RoleOf<C>> = EventsOf<ServerMessages<C, R>>;
150
+ /** A role's subscribable topics (server→client entries with `subscribe: true`). */
151
+ type Topics<C extends Contract, R extends RoleOf<C>> = TopicsOf<ServerMessages<C, R>>;
152
+ /** Requests in the `shared` block (every role can call these). */
153
+ type SharedRequests<C extends Contract> = CtsOf<C['shared']>;
154
+ /** Requests in one role's block (not including `shared`). */
155
+ type RoleRequests<C extends Contract, R extends RoleOf<C>> = CtsOf<C['roles'][R]>;
156
+ /** Push events in the `shared` block (broadcastable to a mixed-role room). */
157
+ type SharedEvents<C extends Contract> = EventsOf<StcOf<C['shared']>>;
158
+ /** Subscribable topics in the `shared` block (published via `srv.publish`). */
159
+ type SharedTopics<C extends Contract> = TopicsOf<StcOf<C['shared']>>;
160
+ /** Subscribable topics in one role's block (published via `srv.forRole(r).publish`). */
161
+ type RoleTopics<C extends Contract, R extends RoleOf<C>> = TopicsOf<StcOf<C['roles'][R]>>;
162
+ /** The `serverToServer` map, or `{}` if the contract has none. */
163
+ type ServerEvents<C extends Contract> = C['serverToServer'] extends Record<string, Schema> ? C['serverToServer'] : {};
164
+ /** The input type a client passes for a request (pre-validation). */
165
+ type ClientInput<T> = T extends RequestDef ? InferIn<T['input']> : never;
166
+ /** The input type a server handler receives for a request (post-validation). */
167
+ type ServerInput<T> = T extends RequestDef ? InferOut<T['input']> : never;
168
+ /** The reply type of a request (server returns / client receives). */
169
+ type Output<T> = T extends RequestDef ? InferOut<T['output']> : never;
170
+ /** The data a client receives for an event/topic (post-validation). */
171
+ type EventData<T> = T extends ServerMessageDef ? InferOut<T['payload']> : never;
172
+ /** The data a server sends for an event/topic (pre-validation). */
173
+ type EmitData<T> = T extends ServerMessageDef ? InferIn<T['payload']> : never;
174
+ /** The data a server sends for a serverToServer event. */
175
+ type ServerEmit<T> = T extends Schema ? InferIn<T> : never;
176
+ /** The data a server receives for a serverToServer event. */
177
+ type ServerData<T> = T extends Schema ? InferOut<T> : never;
178
+ /** Infer a schema's **input** type (what you pass into the validator). */
179
+ type InferIn<S extends Schema> = StandardSchemaV1.InferInput<S>;
180
+ /** Infer a schema's **output** type (the validated result). */
181
+ type InferOut<S extends Schema> = StandardSchemaV1.InferOutput<S>;
182
+ /**
183
+ * Validate a value against a Standard Schema validator (sync or async).
184
+ *
185
+ * @param schema - the validator to run.
186
+ * @param value - the untrusted value to validate.
187
+ * @returns the parsed, typed value.
188
+ * @throws {@link SocketError} with code `VALIDATION` if the value doesn't match.
189
+ */
190
+ declare function validate<S extends Schema>(schema: S, value: unknown): Promise<StandardSchemaV1.InferOutput<S>>;
191
+ /**
192
+ * Synchronous validation for hot paths (e.g. client inbound dispatch).
193
+ *
194
+ * @param schema - the validator to run.
195
+ * @param value - the untrusted value to validate.
196
+ * @returns the parsed, typed value.
197
+ * @throws {@link SocketError} with code `VALIDATION` on mismatch, or `INTERNAL` if the schema is async.
198
+ */
199
+ declare function validateSync<S extends Schema>(schema: S, value: unknown): StandardSchemaV1.InferOutput<S>;
200
+
201
+ /**
202
+ * The wire protocol below is an implementation detail — you rarely touch frames
203
+ * directly. It's exported for adapters, custom transports, and tooling.
204
+ */
205
+ /** Protocol version string, negotiated via the WebSocket subprotocol at upgrade. */
206
+ declare const PROTOCOL = "superline.v1";
207
+ interface ReqFrame {
208
+ t: 'req';
209
+ i: number;
210
+ m: string;
211
+ d: unknown;
212
+ }
213
+ interface SubFrame {
214
+ t: 'sub';
215
+ i: number;
216
+ c: string;
217
+ }
218
+ interface UnsubFrame {
219
+ t: 'unsub';
220
+ c: string;
221
+ }
222
+ type ClientFrame = ReqFrame | SubFrame | UnsubFrame;
223
+ interface ResFrame {
224
+ t: 'res';
225
+ i: number;
226
+ d: unknown;
227
+ }
228
+ interface ErrFrame {
229
+ t: 'err';
230
+ i?: number;
231
+ code: string;
232
+ m: string;
233
+ d?: unknown;
234
+ }
235
+ interface EvtFrame {
236
+ t: 'evt';
237
+ e: string;
238
+ d: unknown;
239
+ }
240
+ interface PubFrame {
241
+ t: 'pub';
242
+ c: string;
243
+ d: unknown;
244
+ }
245
+ type ServerFrame = ResFrame | ErrFrame | EvtFrame | PubFrame;
246
+ type Frame = ClientFrame | ServerFrame;
247
+
248
+ export { type Adapter, type ClientFrame, type ClientInput, type Contract, type Directional, type EmitData, type ErrFrame, type ErrorCode, type EventData, type Events, type EvtFrame, type Frame, type InferIn, type InferOut, type Output, PROTOCOL, type PubFrame, type ReqFrame, type RequestDef, type Requests, type ResFrame, type RoleOf, type RoleRequests, type RoleTopics, type Schema, type Serializer, type ServerData, type ServerEmit, type ServerEvents, type ServerFrame, type ServerInput, type ServerMessageDef, type ServerMessages, type SharedEvents, type SharedRequests, type SharedTopics, SocketError, type SocketErrorCode, type SubFrame, type Topics, type UnsubFrame, defineContract, jsonSerializer, validate, validateSync };
package/dist/index.js ADDED
@@ -0,0 +1,59 @@
1
+ // src/errors.ts
2
+ var SocketError = class extends Error {
3
+ /** The typed error code (e.g. `'FORBIDDEN'`), available on the client. */
4
+ code;
5
+ /** Optional structured data attached to the error, delivered to the client. */
6
+ data;
7
+ /**
8
+ * @param code - a {@link SocketErrorCode} or custom string.
9
+ * @param message - human-readable message (defaults to `code`).
10
+ * @param data - optional structured payload delivered to the client.
11
+ */
12
+ constructor(code, message, data) {
13
+ super(message ?? code);
14
+ this.name = "SocketError";
15
+ this.code = code;
16
+ this.data = data;
17
+ }
18
+ };
19
+
20
+ // src/serializer.ts
21
+ var decoder = new TextDecoder();
22
+ var jsonSerializer = {
23
+ encode: (value) => JSON.stringify(value),
24
+ decode: (data) => JSON.parse(typeof data === "string" ? data : decoder.decode(data))
25
+ };
26
+
27
+ // src/contract.ts
28
+ function defineContract(contract) {
29
+ return contract;
30
+ }
31
+ async function validate(schema, value) {
32
+ let result = schema["~standard"].validate(value);
33
+ if (result instanceof Promise) result = await result;
34
+ if (result.issues) {
35
+ throw new SocketError("VALIDATION", "Validation failed", result.issues);
36
+ }
37
+ return result.value;
38
+ }
39
+ function validateSync(schema, value) {
40
+ const result = schema["~standard"].validate(value);
41
+ if (result instanceof Promise) {
42
+ throw new SocketError("INTERNAL", "Async schema not supported for synchronous validation");
43
+ }
44
+ if (result.issues) {
45
+ throw new SocketError("VALIDATION", "Validation failed", result.issues);
46
+ }
47
+ return result.value;
48
+ }
49
+
50
+ // src/wire.ts
51
+ var PROTOCOL = "superline.v1";
52
+ export {
53
+ PROTOCOL,
54
+ SocketError,
55
+ defineContract,
56
+ jsonSerializer,
57
+ validate,
58
+ validateSync
59
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@super-line/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Shared contract, validation, wire protocol and errors for super-line.",
6
+ "license": "MIT",
7
+ "author": "Mert",
8
+ "keywords": [
9
+ "websocket",
10
+ "typesafe",
11
+ "contract",
12
+ "standard-schema",
13
+ "typescript"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mertdogar/super-line.git",
18
+ "directory": "packages/core"
19
+ },
20
+ "homepage": "https://mertdogar.github.io/super-line/",
21
+ "bugs": "https://github.com/mertdogar/super-line/issues",
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "main": "./dist/index.cjs",
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@standard-schema/spec": "^1.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "zod": "^3.24.1"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup"
55
+ }
56
+ }