@vercube/ws 0.0.22 → 0.0.24
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 +10 -5
- package/dist/index.mjs +214 -253
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
<br>
|
|
4
4
|
<br>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Vercube
|
|
7
|
+
|
|
8
|
+
Next generation HTTP framework
|
|
9
|
+
|
|
10
10
|
<a href="https://www.npmjs.com/package/@vercube/auth">
|
|
11
11
|
<img src="https://img.shields.io/npm/v/%40vercube%2Fauth?style=for-the-badge&logo=npm&color=%23767eff" alt="npm"/>
|
|
12
12
|
</a>
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
<a href="https://github.com/vercube/vercube/blob/main/LICENSE" target="_blank">
|
|
17
17
|
<img src="https://img.shields.io/npm/l/%40vercube%2Fauth?style=for-the-badge&color=%23767eff" alt="License"/>
|
|
18
18
|
</a>
|
|
19
|
+
<a href="https://codecov.io/gh/vercube/vercube" target="_blank">
|
|
20
|
+
<img src="https://img.shields.io/codecov/c/github/vercube/vercube?style=for-the-badge&color=%23767eff" alt="Coverage"/>
|
|
21
|
+
</a>
|
|
19
22
|
<br/>
|
|
20
23
|
<br/>
|
|
21
24
|
</div>
|
|
@@ -23,7 +26,9 @@
|
|
|
23
26
|
An ultra-efficient JavaScript server framework that runs anywhere - Node.js, Bun, or Deno - with unmatched flexibility and complete configurability for developers who refuse to sacrifice speed or control.
|
|
24
27
|
|
|
25
28
|
## <a name="module">Websocket module</a>
|
|
29
|
+
|
|
26
30
|
The Websocket module enables you to set up websocket connections on your application. It offers a collection of decorators that enable you to listen to incoming messages, emit/broadcast messages and more. It works based on namespaces.
|
|
27
31
|
|
|
28
32
|
## <a name="documentation">📖 Documentation</a>
|
|
29
|
-
|
|
33
|
+
|
|
34
|
+
Comprehensive documentation is available at [vercube.dev](https://vercube.dev). There you'll find detailed module descriptions, project information, guides, and everything else you need to know about Vercube.
|
package/dist/index.mjs
CHANGED
|
@@ -1,36 +1,9 @@
|
|
|
1
|
-
import "node:module";
|
|
2
1
|
import { BadRequestError, BasePlugin, HttpServer, ValidationProvider, initializeMetadata, initializeMetadataMethod } from "@vercube/core";
|
|
3
2
|
import { BaseDecorator, Identity, Inject, InjectOptional, createDecorator } from "@vercube/di";
|
|
4
|
-
import { plugin } from "crossws/server";
|
|
5
|
-
import { defineHooks } from "crossws";
|
|
6
3
|
import { Logger } from "@vercube/logger";
|
|
4
|
+
import { defineHooks } from "crossws";
|
|
5
|
+
import { plugin } from "crossws/server";
|
|
7
6
|
|
|
8
|
-
//#region rolldown:runtime
|
|
9
|
-
var __create = Object.create;
|
|
10
|
-
var __defProp = Object.defineProperty;
|
|
11
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
14
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
15
|
-
var __commonJS = (cb, mod) => function() {
|
|
16
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
17
|
-
};
|
|
18
|
-
var __copyProps = (to, from, except, desc) => {
|
|
19
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
20
|
-
key = keys[i];
|
|
21
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
22
|
-
get: ((k) => from[k]).bind(null, key),
|
|
23
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return to;
|
|
27
|
-
};
|
|
28
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
29
|
-
value: mod,
|
|
30
|
-
enumerable: true
|
|
31
|
-
}) : target, mod));
|
|
32
|
-
|
|
33
|
-
//#endregion
|
|
34
7
|
//#region src/Decorators/Namespace.ts
|
|
35
8
|
/**
|
|
36
9
|
* A decorator function for defining a websocket namespace and accepting websocket connections.
|
|
@@ -55,37 +28,215 @@ function Namespace(path) {
|
|
|
55
28
|
//#region src/Types/WebsocketTypes.ts
|
|
56
29
|
let WebsocketTypes;
|
|
57
30
|
(function(_WebsocketTypes) {
|
|
58
|
-
|
|
59
|
-
HandlerAction
|
|
60
|
-
HandlerAction
|
|
61
|
-
return HandlerAction
|
|
31
|
+
_WebsocketTypes.HandlerAction = /* @__PURE__ */ function(HandlerAction) {
|
|
32
|
+
HandlerAction["CONNECTION"] = "connection";
|
|
33
|
+
HandlerAction["MESSAGE"] = "message";
|
|
34
|
+
return HandlerAction;
|
|
62
35
|
}({});
|
|
63
|
-
_WebsocketTypes.HandlerAction = HandlerAction;
|
|
64
36
|
})(WebsocketTypes || (WebsocketTypes = {}));
|
|
65
37
|
|
|
66
38
|
//#endregion
|
|
67
|
-
//#region
|
|
68
|
-
|
|
39
|
+
//#region \0@oxc-project+runtime@0.90.0/helpers/decorate.js
|
|
40
|
+
function __decorate(decorators, target, key, desc) {
|
|
41
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
42
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
43
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
44
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
45
|
+
}
|
|
69
46
|
|
|
70
47
|
//#endregion
|
|
71
|
-
//#region
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
48
|
+
//#region src/Services/WebsocketService.ts
|
|
49
|
+
/**
|
|
50
|
+
* WebsocketService class responsible for dealing with Websocket connections.
|
|
51
|
+
*
|
|
52
|
+
* This class is responsible for:
|
|
53
|
+
* - Registering namespaces and accepting websocket connections for them
|
|
54
|
+
* - Registering event handlers and handling them
|
|
55
|
+
*/
|
|
56
|
+
var WebsocketService = class {
|
|
57
|
+
/**
|
|
58
|
+
* Http Server for injecting the server plugin
|
|
59
|
+
*/
|
|
60
|
+
gHttpServer;
|
|
61
|
+
/**
|
|
62
|
+
* Validation provider for running the schema validation
|
|
63
|
+
*/
|
|
64
|
+
gValidationProvider;
|
|
65
|
+
gLogger;
|
|
66
|
+
/**
|
|
67
|
+
* Internal namespace registry
|
|
68
|
+
*/
|
|
69
|
+
fNamespaces = {};
|
|
70
|
+
/**
|
|
71
|
+
* Internal handlers registry
|
|
72
|
+
*/
|
|
73
|
+
fHandlers = {
|
|
74
|
+
[WebsocketTypes.HandlerAction.CONNECTION]: {},
|
|
75
|
+
[WebsocketTypes.HandlerAction.MESSAGE]: {}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Register a new namespace.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} path - The namespace path to register.
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
registerNamespace(path) {
|
|
84
|
+
if (!this?.fNamespaces?.[path.toLowerCase()]) this.fNamespaces[path.toLowerCase()] = [];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Register a new handler for a namespace or event.
|
|
88
|
+
*
|
|
89
|
+
* @param {WebsocketTypes.HandlerAction} action - The handler action type.
|
|
90
|
+
* @param {string} namespace - The namespace to register the handler for.
|
|
91
|
+
* @param {WebsocketTypes.HandlerAttributes} handler - The handler attributes.
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
registerHandler(action, namespace, handler) {
|
|
95
|
+
const normalizedNamespace = namespace.toLowerCase();
|
|
96
|
+
if (action === WebsocketTypes.HandlerAction.CONNECTION) this.fHandlers[action][normalizedNamespace] = handler;
|
|
97
|
+
if (action === WebsocketTypes.HandlerAction.MESSAGE) {
|
|
98
|
+
const event = handler.event;
|
|
99
|
+
if (!event) {
|
|
100
|
+
this.gLogger?.warn("WebsocketService::registerHandler", `Cannot register message handler without an event name for namespace "${normalizedNamespace}".`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!this.fHandlers[action][normalizedNamespace]) this.fHandlers[action][normalizedNamespace] = {};
|
|
104
|
+
this.fHandlers[action][normalizedNamespace][event] = handler;
|
|
105
|
+
}
|
|
106
|
+
this.registerNamespace(normalizedNamespace);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Broadcast a message to all peers in the same namespace (including the sender).
|
|
110
|
+
*
|
|
111
|
+
* @param {Peer} peer - The sender peer (used to determine the namespace).
|
|
112
|
+
* @param {unknown} message - The message to broadcast.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
broadcast(peer, message) {
|
|
116
|
+
const namespace = peer.namespace?.toLowerCase();
|
|
117
|
+
if (!namespace) return;
|
|
118
|
+
const peers = this.fNamespaces[namespace];
|
|
119
|
+
if (!peers || peers.length === 0) return;
|
|
120
|
+
for (const p of peers) p.send(message);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Emit a message to a single peer.
|
|
124
|
+
*
|
|
125
|
+
* @param {Peer} peer - The peer to send the message to.
|
|
126
|
+
* @param {unknown} message - The message to send.
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
emit(peer, message) {
|
|
130
|
+
peer.send(message);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Broadcast a message to all peers in the same namespace except the sender.
|
|
134
|
+
*
|
|
135
|
+
* @param {Peer} peer - The sender peer (used to determine the namespace).
|
|
136
|
+
* @param {unknown} message - The message to broadcast.
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
broadcastOthers(peer, message) {
|
|
140
|
+
const namespace = peer.namespace?.toLowerCase();
|
|
141
|
+
if (!namespace) return;
|
|
142
|
+
const peers = this.fNamespaces[namespace];
|
|
143
|
+
if (!peers || peers.length === 0) return;
|
|
144
|
+
for (const p of peers) if (p.id !== peer.id) p.send(message);
|
|
78
145
|
}
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Initialize the websocket service and attach the server plugin.
|
|
148
|
+
*
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
initialize() {
|
|
152
|
+
const hooks = defineHooks({
|
|
153
|
+
upgrade: async (request) => {
|
|
154
|
+
const url = new URL(request.url);
|
|
155
|
+
const namespace = url.pathname;
|
|
156
|
+
const parameters = Object.fromEntries(url.searchParams.entries());
|
|
157
|
+
if (!!!this.fNamespaces?.[namespace?.toLowerCase()]) {
|
|
158
|
+
this.gLogger?.warn("WebsocketService::initialize", `Namespace "${namespace}" is not registered. Connection rejected.`);
|
|
159
|
+
return new Response("Namespace not registered", { status: 403 });
|
|
160
|
+
}
|
|
161
|
+
const handler = this.fHandlers[WebsocketTypes.HandlerAction.CONNECTION]?.[namespace];
|
|
162
|
+
if (handler) try {
|
|
163
|
+
if (await handler.callback(parameters, request) === false) return new Response("Unauthorized", { status: 403 });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof Error) return new Response(error.message, { status: 403 });
|
|
166
|
+
return new Response("Unknown error", { status: 403 });
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
namespace,
|
|
170
|
+
headers: {}
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
open: async (peer) => {
|
|
174
|
+
const namespace = peer.namespace?.toLowerCase();
|
|
175
|
+
if (namespace && this.fNamespaces[namespace]) this.fNamespaces[namespace].push(peer);
|
|
176
|
+
},
|
|
177
|
+
message: async (peer, message) => {
|
|
178
|
+
await this.handleMessage(peer, message);
|
|
179
|
+
},
|
|
180
|
+
close: async (peer) => {
|
|
181
|
+
const namespace = peer.namespace?.toLowerCase();
|
|
182
|
+
if (namespace && this.fNamespaces[namespace]) {
|
|
183
|
+
const peers = this.fNamespaces[namespace];
|
|
184
|
+
this.fNamespaces[namespace] = peers.filter((p) => p.id !== peer.id);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
error: async (peer, error) => {
|
|
188
|
+
this.gLogger?.error("WebsocketService::initialize", `Error: ${error.message}`, { peer });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const serverPlugin = plugin(hooks);
|
|
192
|
+
this.gHttpServer.addPlugin(serverPlugin);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Handle an incoming websocket message for a peer.
|
|
196
|
+
*
|
|
197
|
+
* @param {Peer} peer - The peer receiving the message.
|
|
198
|
+
* @param {Message} rawMessage - The raw websocket message.
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
*/
|
|
201
|
+
async handleMessage(peer, rawMessage) {
|
|
202
|
+
try {
|
|
203
|
+
const msg = JSON.parse(rawMessage.text());
|
|
204
|
+
const namespace = peer.namespace?.toLowerCase();
|
|
205
|
+
const event = msg.event;
|
|
206
|
+
const data = msg.data;
|
|
207
|
+
const handler = this.fHandlers[WebsocketTypes.HandlerAction.MESSAGE]?.[namespace]?.[event];
|
|
208
|
+
if (!handler) {
|
|
209
|
+
this.gLogger?.warn("WebsocketService::handleMessage", `No message handler for event "${event}" in namespace "${namespace}"`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (handler.schema) {
|
|
213
|
+
if (!this.gValidationProvider) {
|
|
214
|
+
this.gLogger?.warn("WebsocketService::handleMessage", "ValidationProvider is not registered");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const result = await this.gValidationProvider.validate(handler.schema, data);
|
|
218
|
+
if (result?.issues?.length) throw new BadRequestError("Websocket message validation error", result.issues);
|
|
219
|
+
}
|
|
220
|
+
await handler.callback(data, peer);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.gLogger?.error("WebsocketService::handleMessage", `Failed to process message: ${error}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
__decorate([Inject(HttpServer)], WebsocketService.prototype, "gHttpServer", void 0);
|
|
227
|
+
__decorate([InjectOptional(ValidationProvider)], WebsocketService.prototype, "gValidationProvider", void 0);
|
|
228
|
+
__decorate([InjectOptional(Logger)], WebsocketService.prototype, "gLogger", void 0);
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/Symbols/WebsocketSymbols.ts
|
|
232
|
+
const $WebsocketService = Identity("WebsocketService");
|
|
81
233
|
|
|
82
234
|
//#endregion
|
|
83
235
|
//#region src/Decorators/Message.ts
|
|
84
|
-
var import_decorate$5 = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
85
236
|
/**
|
|
86
237
|
* A decorator class for listening to websocket messages under
|
|
87
238
|
* a specific event.
|
|
88
|
-
*
|
|
239
|
+
*
|
|
89
240
|
* This class extends the BaseDecorator and is used to listen
|
|
90
241
|
* to websocket messages under a specific event.
|
|
91
242
|
*
|
|
@@ -117,7 +268,7 @@ var MessageDecorator = class extends BaseDecorator {
|
|
|
117
268
|
});
|
|
118
269
|
}
|
|
119
270
|
};
|
|
120
|
-
(
|
|
271
|
+
__decorate([InjectOptional($WebsocketService)], MessageDecorator.prototype, "gWebsocketService", void 0);
|
|
121
272
|
/**
|
|
122
273
|
* A decorator function for listening to websocket messages under a specific event.
|
|
123
274
|
*
|
|
@@ -133,7 +284,6 @@ function Message(params) {
|
|
|
133
284
|
|
|
134
285
|
//#endregion
|
|
135
286
|
//#region src/Decorators/Emit.ts
|
|
136
|
-
var import_decorate$4 = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
137
287
|
/**
|
|
138
288
|
* A decorator class for emitting websocket messages to the peer.
|
|
139
289
|
*
|
|
@@ -162,7 +312,7 @@ var EmitDecorator = class extends BaseDecorator {
|
|
|
162
312
|
};
|
|
163
313
|
}
|
|
164
314
|
};
|
|
165
|
-
(
|
|
315
|
+
__decorate([InjectOptional($WebsocketService)], EmitDecorator.prototype, "gWebsocketService", void 0);
|
|
166
316
|
/**
|
|
167
317
|
* A decorator function for emitting websocket messages to the peer.
|
|
168
318
|
*
|
|
@@ -178,11 +328,10 @@ function Emit(event) {
|
|
|
178
328
|
|
|
179
329
|
//#endregion
|
|
180
330
|
//#region src/Decorators/Broadcast.ts
|
|
181
|
-
var import_decorate$3 = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
182
331
|
/**
|
|
183
|
-
* A decorator class for broadcasting websocket messages to everyone
|
|
332
|
+
* A decorator class for broadcasting websocket messages to everyone
|
|
184
333
|
* on the namespace (including the peer).
|
|
185
|
-
*
|
|
334
|
+
*
|
|
186
335
|
* This class extends the BaseDecorator and is used to emit the result of
|
|
187
336
|
* your function as a websocket message to everyone on the namespace.
|
|
188
337
|
*
|
|
@@ -208,7 +357,7 @@ var BroadcastDecorator = class extends BaseDecorator {
|
|
|
208
357
|
};
|
|
209
358
|
}
|
|
210
359
|
};
|
|
211
|
-
(
|
|
360
|
+
__decorate([InjectOptional($WebsocketService)], BroadcastDecorator.prototype, "gWebsocketService", void 0);
|
|
212
361
|
/**
|
|
213
362
|
* A decorator function for broadcasting websocket messages to everyone on the namespace (including the peer).
|
|
214
363
|
*
|
|
@@ -224,11 +373,10 @@ function Broadcast(event) {
|
|
|
224
373
|
|
|
225
374
|
//#endregion
|
|
226
375
|
//#region src/Decorators/BroadcastOthers.ts
|
|
227
|
-
var import_decorate$2 = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
228
376
|
/**
|
|
229
|
-
* A decorator class for broadcasting websocket messages to everyone
|
|
377
|
+
* A decorator class for broadcasting websocket messages to everyone
|
|
230
378
|
* on the namespace (except the peer).
|
|
231
|
-
*
|
|
379
|
+
*
|
|
232
380
|
* This class extends the BaseDecorator and is used to emit the result of
|
|
233
381
|
* your function as a websocket message to everyone on the namespace
|
|
234
382
|
* (except the peer).
|
|
@@ -255,7 +403,7 @@ var BroadcastOthersDecorator = class extends BaseDecorator {
|
|
|
255
403
|
};
|
|
256
404
|
}
|
|
257
405
|
};
|
|
258
|
-
(
|
|
406
|
+
__decorate([InjectOptional($WebsocketService)], BroadcastOthersDecorator.prototype, "gWebsocketService", void 0);
|
|
259
407
|
/**
|
|
260
408
|
* A decorator function for broadcasting websocket messages to everyone on the namespace (except the peer).
|
|
261
409
|
*
|
|
@@ -271,18 +419,17 @@ function BroadcastOthers(event) {
|
|
|
271
419
|
|
|
272
420
|
//#endregion
|
|
273
421
|
//#region src/Decorators/OnConnectionAttempt.ts
|
|
274
|
-
var import_decorate$1 = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
275
422
|
/**
|
|
276
423
|
* A decorator class for handling websocket connection attempts.
|
|
277
424
|
*
|
|
278
425
|
* This class extends the BaseDecorator and is used to handle a
|
|
279
426
|
* websocket connection attempt to a namespace.
|
|
280
|
-
*
|
|
427
|
+
*
|
|
281
428
|
* If your function throws, or returns false, the connection
|
|
282
429
|
* will not be accepted.
|
|
283
|
-
*
|
|
430
|
+
*
|
|
284
431
|
* The decorated function will receive the following parameters:
|
|
285
|
-
*
|
|
432
|
+
*
|
|
286
433
|
* @param {Record<string, unknown>} params - The connection query parameters.
|
|
287
434
|
* @param {Request} request - The original HTTP request.
|
|
288
435
|
*
|
|
@@ -306,7 +453,7 @@ var OnConnectionAttemptDecorator = class extends BaseDecorator {
|
|
|
306
453
|
this.gWebsocketService.registerHandler(WebsocketTypes.HandlerAction.CONNECTION, namespace, { callback: originalMethod });
|
|
307
454
|
}
|
|
308
455
|
};
|
|
309
|
-
(
|
|
456
|
+
__decorate([InjectOptional($WebsocketService)], OnConnectionAttemptDecorator.prototype, "gWebsocketService", void 0);
|
|
310
457
|
/**
|
|
311
458
|
* A decorator function for handling websocket connection attempts to a namespace.
|
|
312
459
|
*
|
|
@@ -324,212 +471,26 @@ function OnConnectionAttempt() {
|
|
|
324
471
|
return createDecorator(OnConnectionAttemptDecorator, void 0);
|
|
325
472
|
}
|
|
326
473
|
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/Services/WebsocketService.ts
|
|
329
|
-
var import_decorate = /* @__PURE__ */ __toESM(require_decorate(), 1);
|
|
330
|
-
/**
|
|
331
|
-
* WebsocketService class responsible for dealing with Websocket connections.
|
|
332
|
-
*
|
|
333
|
-
* This class is responsible for:
|
|
334
|
-
* - Registering namespaces and accepting websocket connections for them
|
|
335
|
-
* - Registering event handlers and handling them
|
|
336
|
-
*/
|
|
337
|
-
var WebsocketService = class {
|
|
338
|
-
/**
|
|
339
|
-
* Http Server for injecting the server plugin
|
|
340
|
-
*/
|
|
341
|
-
gHttpServer;
|
|
342
|
-
/**
|
|
343
|
-
* Validation provider for running the schema validation
|
|
344
|
-
*/
|
|
345
|
-
gValidationProvider;
|
|
346
|
-
gLogger;
|
|
347
|
-
/**
|
|
348
|
-
* Internal namespace registry
|
|
349
|
-
*/
|
|
350
|
-
fNamespaces = {};
|
|
351
|
-
/**
|
|
352
|
-
* Internal handlers registry
|
|
353
|
-
*/
|
|
354
|
-
fHandlers = {
|
|
355
|
-
[WebsocketTypes.HandlerAction.CONNECTION]: {},
|
|
356
|
-
[WebsocketTypes.HandlerAction.MESSAGE]: {}
|
|
357
|
-
};
|
|
358
|
-
/**
|
|
359
|
-
* Register a new namespace.
|
|
360
|
-
*
|
|
361
|
-
* @param {string} path - The namespace path to register.
|
|
362
|
-
* @returns {void}
|
|
363
|
-
*/
|
|
364
|
-
registerNamespace(path) {
|
|
365
|
-
if (!this?.fNamespaces?.[path.toLowerCase()]) this.fNamespaces[path.toLowerCase()] = [];
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Register a new handler for a namespace or event.
|
|
369
|
-
*
|
|
370
|
-
* @param {WebsocketTypes.HandlerAction} action - The handler action type.
|
|
371
|
-
* @param {string} namespace - The namespace to register the handler for.
|
|
372
|
-
* @param {WebsocketTypes.HandlerAttributes} handler - The handler attributes.
|
|
373
|
-
* @returns {void}
|
|
374
|
-
*/
|
|
375
|
-
registerHandler(action, namespace, handler) {
|
|
376
|
-
const normalizedNamespace = namespace.toLowerCase();
|
|
377
|
-
if (action === WebsocketTypes.HandlerAction.CONNECTION) this.fHandlers[action][normalizedNamespace] = handler;
|
|
378
|
-
if (action === WebsocketTypes.HandlerAction.MESSAGE) {
|
|
379
|
-
const event = handler.event;
|
|
380
|
-
if (!event) {
|
|
381
|
-
this.gLogger?.warn("WebsocketService::registerHandler", `Cannot register message handler without an event name for namespace "${normalizedNamespace}".`);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (!this.fHandlers[action][normalizedNamespace]) this.fHandlers[action][normalizedNamespace] = {};
|
|
385
|
-
this.fHandlers[action][normalizedNamespace][event] = handler;
|
|
386
|
-
}
|
|
387
|
-
this.registerNamespace(normalizedNamespace);
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Broadcast a message to all peers in the same namespace (including the sender).
|
|
391
|
-
*
|
|
392
|
-
* @param {Peer} peer - The sender peer (used to determine the namespace).
|
|
393
|
-
* @param {unknown} message - The message to broadcast.
|
|
394
|
-
* @returns {void}
|
|
395
|
-
*/
|
|
396
|
-
broadcast(peer, message) {
|
|
397
|
-
const namespace = peer.namespace?.toLowerCase();
|
|
398
|
-
if (!namespace) return;
|
|
399
|
-
const peers = this.fNamespaces[namespace];
|
|
400
|
-
if (!peers || peers.length === 0) return;
|
|
401
|
-
for (const p of peers) p.send(message);
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Emit a message to a single peer.
|
|
405
|
-
*
|
|
406
|
-
* @param {Peer} peer - The peer to send the message to.
|
|
407
|
-
* @param {unknown} message - The message to send.
|
|
408
|
-
* @returns {void}
|
|
409
|
-
*/
|
|
410
|
-
emit(peer, message) {
|
|
411
|
-
peer.send(message);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Broadcast a message to all peers in the same namespace except the sender.
|
|
415
|
-
*
|
|
416
|
-
* @param {Peer} peer - The sender peer (used to determine the namespace).
|
|
417
|
-
* @param {unknown} message - The message to broadcast.
|
|
418
|
-
* @returns {void}
|
|
419
|
-
*/
|
|
420
|
-
broadcastOthers(peer, message) {
|
|
421
|
-
const namespace = peer.namespace?.toLowerCase();
|
|
422
|
-
if (!namespace) return;
|
|
423
|
-
const peers = this.fNamespaces[namespace];
|
|
424
|
-
if (!peers || peers.length === 0) return;
|
|
425
|
-
for (const p of peers) if (p.id !== peer.id) p.send(message);
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Initialize the websocket service and attach the server plugin.
|
|
429
|
-
*
|
|
430
|
-
* @returns {void}
|
|
431
|
-
*/
|
|
432
|
-
initialize() {
|
|
433
|
-
const hooks = defineHooks({
|
|
434
|
-
upgrade: async (request) => {
|
|
435
|
-
const url = new URL(request.url);
|
|
436
|
-
const namespace = url.pathname;
|
|
437
|
-
const parameters = Object.fromEntries(url.searchParams.entries());
|
|
438
|
-
const isNamespaceRegistered = !!this.fNamespaces?.[namespace?.toLowerCase()];
|
|
439
|
-
if (!isNamespaceRegistered) {
|
|
440
|
-
this.gLogger?.warn("WebsocketService::initialize", `Namespace "${namespace}" is not registered. Connection rejected.`);
|
|
441
|
-
return new Response("Namespace not registered", { status: 403 });
|
|
442
|
-
}
|
|
443
|
-
const handler = this.fHandlers[WebsocketTypes.HandlerAction.CONNECTION]?.[namespace];
|
|
444
|
-
if (handler) try {
|
|
445
|
-
const result = await handler.callback(parameters, request);
|
|
446
|
-
if (result === false) return new Response("Unauthorized", { status: 403 });
|
|
447
|
-
} catch (error) {
|
|
448
|
-
if (error instanceof Error) return new Response(error.message, { status: 403 });
|
|
449
|
-
return new Response("Unknown error", { status: 403 });
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
namespace,
|
|
453
|
-
headers: {}
|
|
454
|
-
};
|
|
455
|
-
},
|
|
456
|
-
open: async (peer) => {
|
|
457
|
-
const namespace = peer.namespace?.toLowerCase();
|
|
458
|
-
if (namespace && this.fNamespaces[namespace]) this.fNamespaces[namespace].push(peer);
|
|
459
|
-
},
|
|
460
|
-
message: async (peer, message) => {
|
|
461
|
-
await this.handleMessage(peer, message);
|
|
462
|
-
},
|
|
463
|
-
close: async (peer) => {
|
|
464
|
-
const namespace = peer.namespace?.toLowerCase();
|
|
465
|
-
if (namespace && this.fNamespaces[namespace]) {
|
|
466
|
-
const peers = this.fNamespaces[namespace];
|
|
467
|
-
this.fNamespaces[namespace] = peers.filter((p) => p.id !== peer.id);
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
error: async (peer, error) => {
|
|
471
|
-
this.gLogger?.error("WebsocketService::initialize", `Error: ${error.message}`, { peer });
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
const serverPlugin = plugin(hooks);
|
|
475
|
-
this.gHttpServer.addPlugin(serverPlugin);
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Handle an incoming websocket message for a peer.
|
|
479
|
-
*
|
|
480
|
-
* @param {Peer} peer - The peer receiving the message.
|
|
481
|
-
* @param {Message} rawMessage - The raw websocket message.
|
|
482
|
-
* @returns {Promise<void>}
|
|
483
|
-
*/
|
|
484
|
-
async handleMessage(peer, rawMessage) {
|
|
485
|
-
try {
|
|
486
|
-
const msg = JSON.parse(rawMessage.text());
|
|
487
|
-
const namespace = peer.namespace?.toLowerCase();
|
|
488
|
-
const event = msg.event;
|
|
489
|
-
const data = msg.data;
|
|
490
|
-
const handler = this.fHandlers[WebsocketTypes.HandlerAction.MESSAGE]?.[namespace]?.[event];
|
|
491
|
-
if (!handler) {
|
|
492
|
-
this.gLogger?.warn("WebsocketService::handleMessage", `No message handler for event "${event}" in namespace "${namespace}"`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (handler.schema) {
|
|
496
|
-
if (!this.gValidationProvider) {
|
|
497
|
-
this.gLogger?.warn("WebsocketService::handleMessage", "ValidationProvider is not registered");
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const result = await this.gValidationProvider.validate(handler.schema, data);
|
|
501
|
-
if (result?.issues?.length) throw new BadRequestError("Websocket message validation error", result.issues);
|
|
502
|
-
}
|
|
503
|
-
await handler.callback(data, peer);
|
|
504
|
-
} catch (error) {
|
|
505
|
-
this.gLogger?.error("WebsocketService::handleMessage", `Failed to process message: ${error}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
(0, import_decorate.default)([Inject(HttpServer)], WebsocketService.prototype, "gHttpServer", void 0);
|
|
510
|
-
(0, import_decorate.default)([InjectOptional(ValidationProvider)], WebsocketService.prototype, "gValidationProvider", void 0);
|
|
511
|
-
(0, import_decorate.default)([InjectOptional(Logger)], WebsocketService.prototype, "gLogger", void 0);
|
|
512
|
-
|
|
513
474
|
//#endregion
|
|
514
475
|
//#region src/Plugins/WebsocketPlugin.ts
|
|
515
476
|
/**
|
|
516
477
|
* Websocket Plugin for Vercube framework
|
|
517
|
-
*
|
|
478
|
+
*
|
|
518
479
|
* Enables websocket connections and use of decorators related
|
|
519
480
|
* to the Websocket package.
|
|
520
|
-
*
|
|
481
|
+
*
|
|
521
482
|
* @example
|
|
522
483
|
* ```ts
|
|
523
484
|
* import { createApp } from '@vercube/core';
|
|
524
485
|
* import { WebsocketPlugin } from '@vercube/ws';
|
|
525
|
-
*
|
|
486
|
+
*
|
|
526
487
|
* const app = createApp({
|
|
527
488
|
* setup: async (app) => {
|
|
528
489
|
* app.addPlugin(WebsocketPlugin);
|
|
529
490
|
* }
|
|
530
491
|
* });
|
|
531
492
|
* ```
|
|
532
|
-
*
|
|
493
|
+
*
|
|
533
494
|
* @see {@link https://vercube.dev} for full documentation
|
|
534
495
|
*/
|
|
535
496
|
var WebsocketPlugin = class extends BasePlugin {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercube/ws",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Websocket module for Vercube framework",
|
|
5
5
|
"repository": "@vercube/ws",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"zod": "4.
|
|
21
|
+
"zod": "4.1.11"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"crossws": "0.4.1",
|
|
25
|
-
"srvx": "0.8.
|
|
26
|
-
"@vercube/core": "0.0.
|
|
27
|
-
"@vercube/
|
|
28
|
-
"@vercube/
|
|
25
|
+
"srvx": "0.8.7",
|
|
26
|
+
"@vercube/core": "0.0.24",
|
|
27
|
+
"@vercube/logger": "0.0.24",
|
|
28
|
+
"@vercube/di": "0.0.24"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|