@zap-socket/server 0.0.16 → 0.0.18
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/server.d.ts +21 -6
- package/dist/server.js +114 -76
- package/dist/utils.js +2 -0
- package/package.json +3 -2
package/dist/server.d.ts
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
import { WebSocketServer, WebSocket } from "ws";
|
2
2
|
import type { EventMap, ZapEvent, ZapServerEvent } from "@zap-socket/types";
|
3
|
-
|
3
|
+
import { ZodType, z } from "zod";
|
4
|
+
type CorsOptions = {
|
5
|
+
origin: string[];
|
6
|
+
methods: string[];
|
7
|
+
headers: string[];
|
8
|
+
credentials: boolean;
|
9
|
+
};
|
10
|
+
type ZapServerConstructorT = {
|
4
11
|
port: number;
|
5
12
|
events?: EventMap;
|
6
|
-
|
13
|
+
cors?: CorsOptions;
|
14
|
+
options?: {
|
15
|
+
heartbeatPingFrequency: number;
|
16
|
+
};
|
17
|
+
};
|
18
|
+
type ExtractSendData<T, K extends keyof T> = T[K] extends ZapServerEvent<any> ? T[K]['data'] : T[K] extends ZapEvent<any, any> ? T[K]['emitType'] extends ZodType<any, any, any> ? z.infer<T[K]['emitType']> : ReturnType<T[K]['process']> extends undefined ? undefined : ReturnType<T[K]['process']> : never;
|
7
19
|
export declare class ZapServer<T extends EventMap> {
|
8
20
|
wss: WebSocketServer;
|
9
21
|
onconnect: (handler: (ctx: {
|
@@ -14,19 +26,22 @@ export declare class ZapServer<T extends EventMap> {
|
|
14
26
|
private wsToId;
|
15
27
|
private idToWs;
|
16
28
|
private _events;
|
17
|
-
|
29
|
+
private heartbeatMiss;
|
30
|
+
constructor({ port, events, options }: ZapServerConstructorT, callback?: () => void);
|
18
31
|
private removeClient;
|
32
|
+
private heartbeat;
|
33
|
+
private handleMessage;
|
19
34
|
sendMessageRaw(clientId: string, data: any): void;
|
20
35
|
sendMessage(event: keyof T, clientId: string, data: any): void;
|
21
36
|
broadcastRaw(data: any): void;
|
22
37
|
broadcast(event: keyof T, data: any): void;
|
23
38
|
selectiveBroascast(event: string, data: any, connections: string[]): void;
|
24
39
|
get events(): { [K in keyof T as T[K] extends ZapServerEvent<any> | ZapEvent<any, any> ? K : never]: {
|
25
|
-
send: (clientId: string, data?:
|
26
|
-
broadcast: (data?:
|
40
|
+
send: (clientId: string, data?: ExtractSendData<T, K>) => void;
|
41
|
+
broadcast: (data?: ExtractSendData<T, K>) => void;
|
27
42
|
}; };
|
28
43
|
get clients(): string[];
|
29
44
|
get socketMap(): Map<string, WebSocket>;
|
30
45
|
}
|
31
|
-
export declare const createZapServer: <T extends EventMap>({ port, events }: ZapServerConstructorT, callback?: () => void) => ZapServer<T>;
|
46
|
+
export declare const createZapServer: <T extends EventMap>({ port, events, cors, options }: ZapServerConstructorT, callback?: () => void) => ZapServer<T>;
|
32
47
|
export {};
|
package/dist/server.js
CHANGED
@@ -4,13 +4,15 @@ const isClientEvent = (event) => {
|
|
4
4
|
return "process" in event; // both zapEvent and zapStream have process in them.
|
5
5
|
};
|
6
6
|
export class ZapServer {
|
7
|
+
// public server: http.Server;
|
7
8
|
wss;
|
8
9
|
onconnect;
|
9
10
|
onconnectHandler;
|
10
11
|
wsToId;
|
11
12
|
idToWs;
|
12
13
|
_events = {};
|
13
|
-
|
14
|
+
heartbeatMiss = new Map();
|
15
|
+
constructor({ port, events = {}, options }, callback) {
|
14
16
|
this.wss = new WebSocketServer({ port });
|
15
17
|
this.wsToId = new Map();
|
16
18
|
this.idToWs = new Map();
|
@@ -19,85 +21,16 @@ export class ZapServer {
|
|
19
21
|
this.onconnect = (handler) => {
|
20
22
|
this.onconnectHandler = handler;
|
21
23
|
};
|
24
|
+
const seconds = options?.heartbeatPingFrequency ?? 5;
|
25
|
+
const frequency = (Number.isFinite(seconds) && seconds > 0 ? seconds : 5) * 1000;
|
26
|
+
setInterval(() => this.heartbeat(), frequency);
|
22
27
|
this.wss.on("listening", () => {
|
23
28
|
if (callback)
|
24
29
|
callback();
|
25
30
|
});
|
26
31
|
this.wss.on("connection", (ws, req) => {
|
27
32
|
ws.on("message", (message) => {
|
28
|
-
|
29
|
-
const id = generateId();
|
30
|
-
this.wsToId.set(ws, id);
|
31
|
-
this.idToWs.set(id, ws);
|
32
|
-
ws.send("ID " + id);
|
33
|
-
this.onconnectHandler({
|
34
|
-
id,
|
35
|
-
ws
|
36
|
-
});
|
37
|
-
return;
|
38
|
-
}
|
39
|
-
const clientId = this.wsToId.get(ws);
|
40
|
-
const parsedMessage = deserialize(message.toString());
|
41
|
-
if (!parsedMessage)
|
42
|
-
return;
|
43
|
-
const { event, stream, data, requestId, streamId, batch } = parsedMessage;
|
44
|
-
const key = event || stream;
|
45
|
-
const eventObj = this._events[key];
|
46
|
-
if (!eventObj || !isClientEvent(eventObj))
|
47
|
-
return;
|
48
|
-
const { process, middleware } = eventObj;
|
49
|
-
// Setup middleware context
|
50
|
-
const ctx = {};
|
51
|
-
if (middleware) {
|
52
|
-
for (const m of middleware) {
|
53
|
-
const metadata = {
|
54
|
-
id: clientId,
|
55
|
-
ip: req.socket.remoteAddress,
|
56
|
-
timestamp: Date.now(),
|
57
|
-
size: message.toString().length,
|
58
|
-
};
|
59
|
-
const msg = {
|
60
|
-
event: key,
|
61
|
-
data: parsedMessage,
|
62
|
-
metadata,
|
63
|
-
};
|
64
|
-
if (!m(ctx, msg))
|
65
|
-
return;
|
66
|
-
}
|
67
|
-
}
|
68
|
-
// All middleware passed
|
69
|
-
const context = { server: this, id: this.wsToId.get(ws), buffer: ctx };
|
70
|
-
if (requestId) { // req-res premitive
|
71
|
-
let result;
|
72
|
-
if (batch) {
|
73
|
-
if (!data) {
|
74
|
-
ws.send("ACK " + requestId);
|
75
|
-
return;
|
76
|
-
}
|
77
|
-
result = data.map((part) => process(part, context));
|
78
|
-
}
|
79
|
-
else {
|
80
|
-
result = process(data, context);
|
81
|
-
}
|
82
|
-
if (result === undefined) { // just ACK the request process returns nothing
|
83
|
-
ws.send("ACK " + requestId);
|
84
|
-
return;
|
85
|
-
}
|
86
|
-
const serialized = serialize({ requestId, event: key, data: result });
|
87
|
-
if (!serialized)
|
88
|
-
return;
|
89
|
-
ws.send(serialized);
|
90
|
-
}
|
91
|
-
else if (streamId) { // stream premitive
|
92
|
-
const consumeStream = async () => {
|
93
|
-
const result = process(data, context);
|
94
|
-
for await (const fragment of result) {
|
95
|
-
this.sendMessageRaw(clientId, { streamId, fragment });
|
96
|
-
}
|
97
|
-
this.sendMessageRaw(clientId, { streamId, done: true });
|
98
|
-
};
|
99
|
-
consumeStream();
|
100
|
-
}
|
33
|
+
this.handleMessage(ws, req, message);
|
101
34
|
});
|
102
35
|
ws.on("close", () => {
|
103
36
|
this.removeClient(ws);
|
@@ -114,6 +47,111 @@ export class ZapServer {
|
|
114
47
|
this.idToWs.delete(clientId);
|
115
48
|
}
|
116
49
|
}
|
50
|
+
heartbeat() {
|
51
|
+
this.idToWs.forEach((ws, id) => {
|
52
|
+
const misses = this.heartbeatMiss.get(id) ?? 0;
|
53
|
+
if (misses > 2) {
|
54
|
+
if (ws)
|
55
|
+
ws.close();
|
56
|
+
this.idToWs.delete(id);
|
57
|
+
this.heartbeatMiss.delete(id);
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
this.heartbeatMiss.set(id, misses + 1);
|
61
|
+
}
|
62
|
+
});
|
63
|
+
this.broadcastRaw("heartbeat");
|
64
|
+
}
|
65
|
+
async handleMessage(ws, req, message) {
|
66
|
+
const id = this.wsToId.get(ws);
|
67
|
+
// setting up socket id
|
68
|
+
if (!id && message.toString() === "OPEN") {
|
69
|
+
const id = generateId();
|
70
|
+
this.wsToId.set(ws, id);
|
71
|
+
this.idToWs.set(id, ws);
|
72
|
+
ws.send("ID " + id);
|
73
|
+
this.onconnectHandler({
|
74
|
+
id,
|
75
|
+
ws
|
76
|
+
});
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
else if (id && message.toString() === "heartbeat") {
|
80
|
+
this.heartbeatMiss.set(id, 0);
|
81
|
+
}
|
82
|
+
const clientId = this.wsToId.get(ws);
|
83
|
+
const parsedMessage = deserialize(message.toString());
|
84
|
+
if (!parsedMessage)
|
85
|
+
return;
|
86
|
+
const { event, stream, data, requestId, streamId, batch } = parsedMessage;
|
87
|
+
const key = event || stream;
|
88
|
+
const eventObj = this._events[key];
|
89
|
+
if (!eventObj || !isClientEvent(eventObj))
|
90
|
+
return;
|
91
|
+
// Type validation.
|
92
|
+
const inputType = eventObj.input;
|
93
|
+
const { success, error } = inputType.safeParse(data);
|
94
|
+
if (!success && error) {
|
95
|
+
// check if the message is of req-res
|
96
|
+
if (requestId) {
|
97
|
+
}
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
const { process, middleware } = eventObj;
|
101
|
+
// Setup middleware context
|
102
|
+
const ctx = {};
|
103
|
+
if (middleware) {
|
104
|
+
for (const m of middleware) {
|
105
|
+
const metadata = {
|
106
|
+
id: clientId,
|
107
|
+
ip: req.socket.remoteAddress,
|
108
|
+
timestamp: Date.now(),
|
109
|
+
size: message.toString().length,
|
110
|
+
};
|
111
|
+
const msg = {
|
112
|
+
event: key,
|
113
|
+
data: parsedMessage,
|
114
|
+
metadata,
|
115
|
+
};
|
116
|
+
let shouldPass = m(ctx, msg);
|
117
|
+
shouldPass = shouldPass instanceof Promise ? await shouldPass : shouldPass;
|
118
|
+
if (!shouldPass)
|
119
|
+
return;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
// All middleware passed
|
123
|
+
const context = { server: this, id: this.wsToId.get(ws), buffer: ctx };
|
124
|
+
if (requestId) { // req-res premitive
|
125
|
+
let result;
|
126
|
+
if (batch) {
|
127
|
+
result = data.map((part) => process(part, context));
|
128
|
+
}
|
129
|
+
else {
|
130
|
+
result = process(data, context);
|
131
|
+
if (result instanceof Promise) {
|
132
|
+
result = await result;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
if (result === undefined) { // just ACK the request process returns nothing
|
136
|
+
ws.send("ACK " + requestId);
|
137
|
+
return;
|
138
|
+
}
|
139
|
+
const serialized = serialize({ requestId, event: key, data: result });
|
140
|
+
if (!serialized)
|
141
|
+
return;
|
142
|
+
ws.send(serialized);
|
143
|
+
}
|
144
|
+
else if (streamId) { // stream premitive
|
145
|
+
const consumeStream = async () => {
|
146
|
+
const result = process(data, context);
|
147
|
+
for await (const fragment of result) {
|
148
|
+
this.sendMessageRaw(clientId, { streamId, fragment });
|
149
|
+
}
|
150
|
+
this.sendMessageRaw(clientId, { streamId, done: true });
|
151
|
+
};
|
152
|
+
consumeStream();
|
153
|
+
}
|
154
|
+
}
|
117
155
|
sendMessageRaw(clientId, data) {
|
118
156
|
const ws = this.idToWs.get(clientId);
|
119
157
|
// TODO: throw a nice error
|
@@ -209,7 +247,7 @@ export class ZapServer {
|
|
209
247
|
return this.idToWs;
|
210
248
|
}
|
211
249
|
}
|
212
|
-
export const createZapServer = ({ port, events }, callback) => {
|
213
|
-
const server = new ZapServer({ port, events }, callback);
|
250
|
+
export const createZapServer = ({ port, events, cors, options }, callback) => {
|
251
|
+
const server = new ZapServer({ port, events, cors, options }, callback);
|
214
252
|
return server;
|
215
253
|
};
|
package/dist/utils.js
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@zap-socket/server",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.18",
|
4
4
|
"description": "A fully typesafe tRPC-inspired WebSocket library with Zod validation, req-res model, and native subscriptions.",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"types": "dist/index.d.ts",
|
@@ -23,7 +23,8 @@
|
|
23
23
|
"vitest": "^3.0.9"
|
24
24
|
},
|
25
25
|
"dependencies": {
|
26
|
-
"@zap-socket/types": "^0.0.
|
26
|
+
"@zap-socket/types": "^0.0.9",
|
27
|
+
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.51.0",
|
27
28
|
"zod": "^3.24.2"
|
28
29
|
}
|
29
30
|
}
|