@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 CHANGED
@@ -1,9 +1,21 @@
1
1
  import { WebSocketServer, WebSocket } from "ws";
2
2
  import type { EventMap, ZapEvent, ZapServerEvent } from "@zap-socket/types";
3
- interface ZapServerConstructorT {
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
- constructor({ port, events }: ZapServerConstructorT, callback?: () => void);
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?: (T[K] extends ZapServerEvent<any> ? T[K]["data"] : T[K] extends ZapEvent<any, any> ? ReturnType<T[K]["process"]> : never)) => void;
26
- broadcast: (data?: (T[K] extends ZapServerEvent<any> ? T[K]["data"] : T[K] extends ZapEvent<any, any> ? ReturnType<T[K]["process"]> : never)) => void;
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
- constructor({ port, events = {} }, callback) {
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
- if (!this.wsToId.get(ws) && message.toString() === "OPEN") {
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
@@ -13,6 +13,8 @@ export const safeJsonParse = (jsonString) => {
13
13
  }
14
14
  };
15
15
  export const serialize = (data) => {
16
+ if (typeof data === "string")
17
+ return data;
16
18
  try {
17
19
  return JSON.stringify(data);
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-socket/server",
3
- "version": "0.0.16",
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.7",
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
  }