@forklaunch/ws 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/lib/index.js ADDED
@@ -0,0 +1,521 @@
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
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CLOSED: () => CLOSED,
24
+ CLOSING: () => CLOSING,
25
+ CONNECTING: () => CONNECTING,
26
+ EventEmitter: () => import_events.EventEmitter,
27
+ ForklaunchWebSocket: () => ForklaunchWebSocket,
28
+ ForklaunchWebSocketServer: () => ForklaunchWebSocketServer,
29
+ OPEN: () => OPEN,
30
+ WebSocket: () => import_ws4.WebSocket,
31
+ WebSocketServer: () => import_ws4.WebSocketServer,
32
+ default: () => ForklaunchWebSocket
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/webSocket.ts
37
+ var import_ws = require("@forklaunch/core/ws");
38
+ var import_ws2 = require("ws");
39
+ var ForklaunchWebSocket = class extends import_ws2.WebSocket {
40
+ eventSchemas;
41
+ schemas;
42
+ schemaValidator;
43
+ /**
44
+ * Creates a new ForklaunchWebSocket instance with schema validation.
45
+ *
46
+ * @param schemaValidator - The schema validator instance (e.g., ZodSchemaValidator)
47
+ * @param eventSchemas - Schema definitions for all WebSocket events
48
+ * @param websocketParams - Standard WebSocket constructor parameters (address, protocols, options)
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const ws = new ForklaunchWebSocket(
53
+ * validator,
54
+ * schemas,
55
+ * 'ws://localhost:8080',
56
+ * ['chat-protocol'],
57
+ * { handshakeTimeout: 5000 }
58
+ * );
59
+ * ```
60
+ */
61
+ constructor(schemaValidator, eventSchemas, ...websocketParams) {
62
+ super(...websocketParams);
63
+ this.schemaValidator = schemaValidator;
64
+ this.eventSchemas = eventSchemas;
65
+ this.schemas = (0, import_ws.createWebSocketSchemas)(
66
+ schemaValidator,
67
+ eventSchemas
68
+ );
69
+ }
70
+ /**
71
+ * Decodes and validates incoming data from the WebSocket.
72
+ *
73
+ * This method handles multiple data formats:
74
+ * - Buffer → decoded to UTF-8 string → parsed as JSON → validated
75
+ * - ArrayBuffer → converted to Buffer → decoded → parsed → validated
76
+ * - TypedArray → converted to Buffer → decoded → parsed → validated
77
+ * - String → parsed as JSON → validated
78
+ * - Object → validated directly (already parsed)
79
+ *
80
+ * @param data - The raw data received from the WebSocket
81
+ * @param schema - Optional schema to validate against
82
+ * @returns The validated and decoded data
83
+ * @throws {Error} If validation fails with pretty-printed error messages
84
+ *
85
+ * @internal
86
+ */
87
+ decodeAndValidate(data, schema) {
88
+ return (0, import_ws.decodeSchemaValue)(
89
+ this.schemaValidator,
90
+ data,
91
+ schema,
92
+ "web socket event"
93
+ );
94
+ }
95
+ /**
96
+ * Validates and encodes outgoing data for transmission over the WebSocket.
97
+ *
98
+ * This method:
99
+ * 1. Validates data against the provided schema (if present)
100
+ * 2. Encodes data to Buffer format:
101
+ * - Buffer → returned as-is
102
+ * - Object/Array → JSON.stringify → Buffer
103
+ * - String → Buffer
104
+ * - Number/Boolean → JSON.stringify → Buffer
105
+ * - null/undefined → returned as-is
106
+ *
107
+ * @param data - The data to validate and encode
108
+ * @param schema - Optional schema to validate against
109
+ * @returns The validated and encoded data as Buffer, or null/undefined
110
+ * @throws {Error} If validation fails with pretty-printed error messages
111
+ *
112
+ * @internal
113
+ */
114
+ validateAndEncode(data, schema, allowUndefined = false, context = "web socket event") {
115
+ const encoded = (0, import_ws.encodeSchemaValue)(
116
+ this.schemaValidator,
117
+ data,
118
+ schema,
119
+ context
120
+ );
121
+ return (0, import_ws.normalizeEncodedValue)(encoded, context, allowUndefined);
122
+ }
123
+ /**
124
+ * Transforms incoming event arguments by decoding and validating them.
125
+ *
126
+ * Applies transformation for:
127
+ * - `message` events: validates message data
128
+ * - `close` events: validates close reason
129
+ * - `ping` events: validates ping data
130
+ * - `pong` events: validates pong data
131
+ *
132
+ * @param event - The event name
133
+ * @param args - The raw event arguments
134
+ * @returns Transformed and validated arguments
135
+ *
136
+ * @internal
137
+ */
138
+ transformIncomingArgs(event, args) {
139
+ let transformedArgs = args;
140
+ if (event === "message" && args.length >= 2) {
141
+ const data = args[0];
142
+ const isBinary = args[1];
143
+ const serverSchema = this.schemas.serverMessagesSchema;
144
+ if (typeof isBinary === "boolean" && isBinary && serverSchema) {
145
+ const validated = this.decodeAndValidate(data, serverSchema);
146
+ transformedArgs = [validated, false, ...args.slice(2)];
147
+ }
148
+ } else if (event === "close" && args.length >= 2) {
149
+ const code = args[0];
150
+ const reason = args[1];
151
+ const validated = this.decodeAndValidate(
152
+ reason,
153
+ this.schemas.closeReasonSchema
154
+ );
155
+ transformedArgs = [code, validated, ...args.slice(2)];
156
+ } else if (event === "ping" && args.length >= 1) {
157
+ const data = args[0];
158
+ const validated = this.decodeAndValidate(data, this.schemas.pingSchema);
159
+ transformedArgs = [validated, ...args.slice(1)];
160
+ } else if (event === "pong" && args.length >= 1) {
161
+ const data = args[0];
162
+ const validated = this.decodeAndValidate(data, this.schemas.pongSchema);
163
+ transformedArgs = [validated, ...args.slice(1)];
164
+ }
165
+ return transformedArgs;
166
+ }
167
+ /**
168
+ * Wraps an event listener with data transformation logic.
169
+ *
170
+ * This helper intercepts listener registration to inject automatic
171
+ * decoding and validation of incoming event data before passing it
172
+ * to the user's listener function.
173
+ *
174
+ * @param superMethod - The parent class method to call (super.on, super.once, etc.)
175
+ * @param event - The event name
176
+ * @param listener - The user's listener function
177
+ * @returns `this` for method chaining
178
+ *
179
+ * @internal
180
+ */
181
+ wrapListenerWithTransformation(superMethod, event, listener) {
182
+ return superMethod(event, (ws, ...args) => {
183
+ const transformedArgs = this.transformIncomingArgs(event, args);
184
+ return listener(ws, ...transformedArgs);
185
+ });
186
+ }
187
+ on(event, listener) {
188
+ return this.wrapListenerWithTransformation(
189
+ super.on.bind(this),
190
+ event,
191
+ listener
192
+ );
193
+ }
194
+ once(event, listener) {
195
+ return this.wrapListenerWithTransformation(
196
+ super.once.bind(this),
197
+ event,
198
+ listener
199
+ );
200
+ }
201
+ off(event, listener) {
202
+ return this.wrapListenerWithTransformation(
203
+ super.off.bind(this),
204
+ event,
205
+ listener
206
+ );
207
+ }
208
+ addListener(event, listener) {
209
+ return this.wrapListenerWithTransformation(
210
+ super.addListener.bind(this),
211
+ event,
212
+ listener
213
+ );
214
+ }
215
+ removeListener(event, listener) {
216
+ return this.wrapListenerWithTransformation(
217
+ super.removeListener.bind(this),
218
+ event,
219
+ listener
220
+ );
221
+ }
222
+ /**
223
+ * Emits an event with automatic data validation and encoding.
224
+ *
225
+ * All outgoing data is automatically:
226
+ * - Validated against the provided schemas
227
+ * - Encoded from JavaScript objects to Buffer format
228
+ * - Transmitted over the WebSocket connection
229
+ *
230
+ * @param event - The event name to emit
231
+ * @param args - The event arguments (type-checked based on event name)
232
+ * @returns `true` if the event had listeners, `false` otherwise
233
+ *
234
+ * @example Emit a message
235
+ * ```typescript
236
+ * // Data is validated against clientMessages schema and auto-encoded
237
+ * ws.emit('message', { type: 'chat', text: 'Hello!' }, true);
238
+ * ```
239
+ *
240
+ * @example Emit other events
241
+ * ```typescript
242
+ * ws.emit('ping', { timestamp: Date.now() });
243
+ * ws.emit('close', 1000, { reason: 'Normal closure' });
244
+ * ws.emit('error', { code: 500, message: 'Server error' });
245
+ * ```
246
+ */
247
+ emit(event, ...args) {
248
+ let transformedArgs = args;
249
+ if (event === "message" && args.length >= 2) {
250
+ const typedArgs = args;
251
+ const data = typedArgs[0];
252
+ const isBinary = typedArgs[1];
253
+ const clientSchema = this.schemas.clientMessagesSchema;
254
+ if (typeof isBinary === "boolean" && isBinary && clientSchema) {
255
+ const encoded = this.validateAndEncode(
256
+ data,
257
+ clientSchema,
258
+ false,
259
+ "web socket message"
260
+ );
261
+ transformedArgs = [encoded, true, ...typedArgs.slice(2)];
262
+ }
263
+ } else if (event === "close" && args.length >= 2) {
264
+ const typedArgs = args;
265
+ const code = typedArgs[0];
266
+ const reason = typedArgs[1];
267
+ const encoded = this.validateAndEncode(
268
+ reason,
269
+ this.schemas.closeReasonSchema,
270
+ false,
271
+ "web socket close"
272
+ );
273
+ transformedArgs = [code, encoded, ...typedArgs.slice(2)];
274
+ } else if (event === "ping" && args.length >= 1) {
275
+ const typedArgs = args;
276
+ const data = typedArgs[0];
277
+ const encoded = this.validateAndEncode(
278
+ data,
279
+ this.schemas.pingSchema,
280
+ false,
281
+ "web socket ping"
282
+ );
283
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
284
+ } else if (event === "pong" && args.length >= 1) {
285
+ const typedArgs = args;
286
+ const data = typedArgs[0];
287
+ const encoded = this.validateAndEncode(
288
+ data,
289
+ this.schemas.pongSchema,
290
+ false,
291
+ "web socket pong"
292
+ );
293
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
294
+ } else if (event === "error" && args.length >= 1) {
295
+ const typedArgs = args;
296
+ const error = typedArgs[0];
297
+ const errorsSchema = this.schemas.errorsSchema;
298
+ if (errorsSchema) {
299
+ const encoded = this.validateAndEncode(
300
+ error,
301
+ errorsSchema,
302
+ false,
303
+ "web socket error"
304
+ );
305
+ transformedArgs = [encoded, ...typedArgs.slice(1)];
306
+ }
307
+ }
308
+ return super.emit(event, ...transformedArgs);
309
+ }
310
+ // @ts-expect-error - Implementation accepts unknown for internal validation
311
+ send(data, optionsOrCb, cb) {
312
+ const options = typeof optionsOrCb === "function" ? void 0 : optionsOrCb;
313
+ const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
314
+ const encoded = this.validateAndEncode(
315
+ data,
316
+ this.schemas.clientMessagesSchema,
317
+ false,
318
+ "web socket message"
319
+ );
320
+ if (!encoded) {
321
+ throw new Error("Invalid data");
322
+ }
323
+ return super.send(encoded, options || {}, callback);
324
+ }
325
+ /**
326
+ * Closes the WebSocket connection with optional validated close reason.
327
+ *
328
+ * @param code - Optional close code (default: 1000 for normal closure)
329
+ * @param reason - Optional close reason (validated against closeReason schema)
330
+ *
331
+ * @example Close with default code
332
+ * ```typescript
333
+ * ws.close();
334
+ * ```
335
+ *
336
+ * @example Close with code and reason
337
+ * ```typescript
338
+ * ws.close(1000, { reason: 'User logged out' });
339
+ * ```
340
+ *
341
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code|WebSocket Close Codes}
342
+ */
343
+ // @ts-expect-error - Intentionally restricting types for compile-time schema validation
344
+ close(code, reason) {
345
+ if (reason) {
346
+ const encoded = this.validateAndEncode(
347
+ reason,
348
+ this.schemas.closeReasonSchema,
349
+ false,
350
+ "web socket close"
351
+ );
352
+ if (encoded) return super.close(code, encoded);
353
+ }
354
+ return super.close(code);
355
+ }
356
+ /**
357
+ * Sends a ping frame with optional validated data payload.
358
+ *
359
+ * Ping frames are used to check if the connection is alive. The server
360
+ * should respond with a pong frame.
361
+ *
362
+ * @param data - Optional ping data (validated against ping schema)
363
+ * @param mask - Whether to mask the frame (default: true for clients)
364
+ * @param cb - Optional callback invoked when ping is sent or errors
365
+ *
366
+ * @example Send a ping
367
+ * ```typescript
368
+ * ws.ping({ timestamp: Date.now() });
369
+ * ```
370
+ *
371
+ * @example Send ping with callback
372
+ * ```typescript
373
+ * ws.ping({ ts: Date.now() }, true, (error) => {
374
+ * if (error) console.error('Ping failed:', error);
375
+ * });
376
+ * ```
377
+ */
378
+ ping(data, mask, cb) {
379
+ if (data !== void 0) {
380
+ const encoded = this.validateAndEncode(
381
+ data,
382
+ this.schemas.pingSchema,
383
+ false,
384
+ "web socket ping"
385
+ );
386
+ super.ping(encoded, mask, cb);
387
+ } else {
388
+ super.ping(void 0, mask, cb);
389
+ }
390
+ }
391
+ /**
392
+ * Sends a pong frame with optional validated data payload.
393
+ *
394
+ * Pong frames are typically sent in response to ping frames, but can
395
+ * also be sent unsolicited as a unidirectional heartbeat.
396
+ *
397
+ * @param data - Optional pong data (validated against pong schema)
398
+ * @param mask - Whether to mask the frame (default: true for clients)
399
+ * @param cb - Optional callback invoked when pong is sent or errors
400
+ *
401
+ * @example Send a pong
402
+ * ```typescript
403
+ * ws.pong({ timestamp: Date.now() });
404
+ * ```
405
+ *
406
+ * @example Respond to ping
407
+ * ```typescript
408
+ * ws.on('ping', (data) => {
409
+ * console.log('Received ping:', data);
410
+ * ws.pong(data); // Echo the ping data back
411
+ * });
412
+ * ```
413
+ */
414
+ pong(data, mask, cb) {
415
+ if (data !== void 0) {
416
+ const encoded = this.validateAndEncode(
417
+ data,
418
+ this.schemas.pongSchema,
419
+ false,
420
+ "web socket pong"
421
+ );
422
+ super.pong(encoded, mask, cb);
423
+ } else {
424
+ super.pong(void 0, mask, cb);
425
+ }
426
+ }
427
+ };
428
+
429
+ // src/webSocketServer.ts
430
+ var import_ws3 = require("ws");
431
+ var ForklaunchWebSocketServer = class extends import_ws3.WebSocketServer {
432
+ /**
433
+ * Creates a new ForklaunchWebSocketServer with schema validation.
434
+ *
435
+ * @param _schemaValidator - The schema validator instance (e.g., ZodSchemaValidator)
436
+ * @param _eventSchemas - Schema definitions for all WebSocket events
437
+ * @param options - Standard WebSocketServer options (port, host, server, etc.)
438
+ * @param callback - Optional callback invoked when the server starts listening
439
+ *
440
+ * @example Create a server on port 8080
441
+ * ```typescript
442
+ * const wss = new ForklaunchWebSocketServer(
443
+ * validator,
444
+ * schemas,
445
+ * { port: 8080 },
446
+ * () => console.log('Server started on port 8080')
447
+ * );
448
+ * ```
449
+ *
450
+ * @example Create a server with existing HTTP server
451
+ * ```typescript
452
+ * import { createServer } from 'http';
453
+ *
454
+ * const httpServer = createServer();
455
+ * const wss = new ForklaunchWebSocketServer(
456
+ * validator,
457
+ * schemas,
458
+ * { server: httpServer }
459
+ * );
460
+ *
461
+ * httpServer.listen(8080);
462
+ * ```
463
+ *
464
+ * @example No server mode (for upgrade handling)
465
+ * ```typescript
466
+ * const wss = new ForklaunchWebSocketServer(
467
+ * validator,
468
+ * schemas,
469
+ * { noServer: true }
470
+ * );
471
+ *
472
+ * httpServer.on('upgrade', (request, socket, head) => {
473
+ * wss.handleUpgrade(request, socket, head, (ws) => {
474
+ * wss.emit('connection', ws, request);
475
+ * });
476
+ * });
477
+ * ```
478
+ */
479
+ constructor(_schemaValidator, _eventSchemas, options, callback) {
480
+ super(options, callback);
481
+ }
482
+ on(event, listener) {
483
+ return super.on(event, listener);
484
+ }
485
+ once(event, listener) {
486
+ return super.once(event, listener);
487
+ }
488
+ off(event, listener) {
489
+ return super.off(event, listener);
490
+ }
491
+ addListener(event, listener) {
492
+ return super.addListener(event, listener);
493
+ }
494
+ removeListener(event, listener) {
495
+ return super.removeListener(
496
+ event,
497
+ listener
498
+ );
499
+ }
500
+ };
501
+
502
+ // index.ts
503
+ var import_ws4 = require("ws");
504
+ var import_ws5 = require("ws");
505
+ var import_events = require("events");
506
+ var CLOSED = import_ws5.WebSocket.CLOSED;
507
+ var CLOSING = import_ws5.WebSocket.CLOSING;
508
+ var CONNECTING = import_ws5.WebSocket.CONNECTING;
509
+ var OPEN = import_ws5.WebSocket.OPEN;
510
+ // Annotate the CommonJS export names for ESM import in node:
511
+ 0 && (module.exports = {
512
+ CLOSED,
513
+ CLOSING,
514
+ CONNECTING,
515
+ EventEmitter,
516
+ ForklaunchWebSocket,
517
+ ForklaunchWebSocketServer,
518
+ OPEN,
519
+ WebSocket,
520
+ WebSocketServer
521
+ });