alepha 0.13.0 → 0.13.1
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/api-jobs/index.d.ts +26 -26
- package/dist/api-users/index.d.ts +1 -1
- package/dist/cli/{dist-Sz2EXvQX.cjs → dist-Dl9Vl7Ur.js} +17 -13
- package/dist/cli/{dist-BBPjuQ56.js.map → dist-Dl9Vl7Ur.js.map} +1 -1
- package/dist/cli/index.d.ts +3 -11
- package/dist/cli/index.js +106 -74
- package/dist/cli/index.js.map +1 -1
- package/dist/email/index.js +71 -73
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +4 -4
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.js +2 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/security/index.d.ts +28 -28
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server-health/index.d.ts +17 -17
- package/dist/server-metrics/index.js +170 -174
- package/dist/server-metrics/index.js.map +1 -1
- package/dist/server-security/index.d.ts +9 -9
- package/dist/vite/index.js +4 -5
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +7 -7
- package/package.json +52 -103
- package/src/cli/apps/AlephaPackageBuilderCli.ts +7 -2
- package/src/cli/assets/appRouterTs.ts +9 -0
- package/src/cli/assets/indexHtml.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +10 -0
- package/src/cli/commands/CoreCommands.ts +6 -5
- package/src/cli/commands/DrizzleCommands.ts +65 -57
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/services/ProjectUtils.ts +44 -38
- package/src/orm/providers/DrizzleKitProvider.ts +1 -1
- package/src/retry/descriptors/$retry.ts +5 -3
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/vite/helpers/boot.ts +3 -3
- package/dist/api-files/index.cjs +0 -1293
- package/dist/api-files/index.cjs.map +0 -1
- package/dist/api-files/index.d.cts +0 -829
- package/dist/api-jobs/index.cjs +0 -274
- package/dist/api-jobs/index.cjs.map +0 -1
- package/dist/api-jobs/index.d.cts +0 -654
- package/dist/api-notifications/index.cjs +0 -380
- package/dist/api-notifications/index.cjs.map +0 -1
- package/dist/api-notifications/index.d.cts +0 -289
- package/dist/api-parameters/index.cjs +0 -66
- package/dist/api-parameters/index.cjs.map +0 -1
- package/dist/api-parameters/index.d.cts +0 -84
- package/dist/api-users/index.cjs +0 -6009
- package/dist/api-users/index.cjs.map +0 -1
- package/dist/api-users/index.d.cts +0 -4740
- package/dist/api-verifications/index.cjs +0 -407
- package/dist/api-verifications/index.cjs.map +0 -1
- package/dist/api-verifications/index.d.cts +0 -207
- package/dist/batch/index.cjs +0 -408
- package/dist/batch/index.cjs.map +0 -1
- package/dist/batch/index.d.cts +0 -330
- package/dist/bin/index.cjs +0 -17
- package/dist/bin/index.cjs.map +0 -1
- package/dist/bin/index.d.cts +0 -1
- package/dist/bucket/index.cjs +0 -303
- package/dist/bucket/index.cjs.map +0 -1
- package/dist/bucket/index.d.cts +0 -355
- package/dist/cache/index.cjs +0 -241
- package/dist/cache/index.cjs.map +0 -1
- package/dist/cache/index.d.cts +0 -202
- package/dist/cache-redis/index.cjs +0 -84
- package/dist/cache-redis/index.cjs.map +0 -1
- package/dist/cache-redis/index.d.cts +0 -40
- package/dist/cli/chunk-DSlc6foC.cjs +0 -43
- package/dist/cli/dist-BBPjuQ56.js +0 -2778
- package/dist/cli/dist-Sz2EXvQX.cjs.map +0 -1
- package/dist/cli/index.cjs +0 -1241
- package/dist/cli/index.cjs.map +0 -1
- package/dist/cli/index.d.cts +0 -422
- package/dist/command/index.cjs +0 -693
- package/dist/command/index.cjs.map +0 -1
- package/dist/command/index.d.cts +0 -340
- package/dist/core/index.cjs +0 -2264
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -1927
- package/dist/datetime/index.cjs +0 -318
- package/dist/datetime/index.cjs.map +0 -1
- package/dist/datetime/index.d.cts +0 -145
- package/dist/email/index.cjs +0 -10874
- package/dist/email/index.cjs.map +0 -1
- package/dist/email/index.d.cts +0 -186
- package/dist/fake/index.cjs +0 -34641
- package/dist/fake/index.cjs.map +0 -1
- package/dist/fake/index.d.cts +0 -74
- package/dist/file/index.cjs +0 -1212
- package/dist/file/index.cjs.map +0 -1
- package/dist/file/index.d.cts +0 -698
- package/dist/lock/index.cjs +0 -226
- package/dist/lock/index.cjs.map +0 -1
- package/dist/lock/index.d.cts +0 -361
- package/dist/lock-redis/index.cjs +0 -113
- package/dist/lock-redis/index.cjs.map +0 -1
- package/dist/lock-redis/index.d.cts +0 -24
- package/dist/logger/index.cjs +0 -521
- package/dist/logger/index.cjs.map +0 -1
- package/dist/logger/index.d.cts +0 -281
- package/dist/orm/index.cjs +0 -2986
- package/dist/orm/index.cjs.map +0 -1
- package/dist/orm/index.d.cts +0 -2213
- package/dist/queue/index.cjs +0 -1044
- package/dist/queue/index.cjs.map +0 -1
- package/dist/queue/index.d.cts +0 -1265
- package/dist/queue-redis/index.cjs +0 -873
- package/dist/queue-redis/index.cjs.map +0 -1
- package/dist/queue-redis/index.d.cts +0 -82
- package/dist/redis/index.cjs +0 -153
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -82
- package/dist/retry/index.cjs +0 -146
- package/dist/retry/index.cjs.map +0 -1
- package/dist/retry/index.d.cts +0 -172
- package/dist/router/index.cjs +0 -111
- package/dist/router/index.cjs.map +0 -1
- package/dist/router/index.d.cts +0 -46
- package/dist/scheduler/index.cjs +0 -576
- package/dist/scheduler/index.cjs.map +0 -1
- package/dist/scheduler/index.d.cts +0 -145
- package/dist/security/index.cjs +0 -2402
- package/dist/security/index.cjs.map +0 -1
- package/dist/security/index.d.cts +0 -598
- package/dist/server/index.cjs +0 -1680
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.d.cts +0 -810
- package/dist/server-auth/index.cjs +0 -3146
- package/dist/server-auth/index.cjs.map +0 -1
- package/dist/server-auth/index.d.cts +0 -1164
- package/dist/server-cache/index.cjs +0 -252
- package/dist/server-cache/index.cjs.map +0 -1
- package/dist/server-cache/index.d.cts +0 -164
- package/dist/server-compress/index.cjs +0 -141
- package/dist/server-compress/index.cjs.map +0 -1
- package/dist/server-compress/index.d.cts +0 -38
- package/dist/server-cookies/index.cjs +0 -234
- package/dist/server-cookies/index.cjs.map +0 -1
- package/dist/server-cookies/index.d.cts +0 -144
- package/dist/server-cors/index.cjs +0 -201
- package/dist/server-cors/index.cjs.map +0 -1
- package/dist/server-cors/index.d.cts +0 -140
- package/dist/server-health/index.cjs +0 -62
- package/dist/server-health/index.cjs.map +0 -1
- package/dist/server-health/index.d.cts +0 -58
- package/dist/server-helmet/index.cjs +0 -131
- package/dist/server-helmet/index.cjs.map +0 -1
- package/dist/server-helmet/index.d.cts +0 -97
- package/dist/server-links/index.cjs +0 -992
- package/dist/server-links/index.cjs.map +0 -1
- package/dist/server-links/index.d.cts +0 -513
- package/dist/server-metrics/index.cjs +0 -4535
- package/dist/server-metrics/index.cjs.map +0 -1
- package/dist/server-metrics/index.d.cts +0 -35
- package/dist/server-multipart/index.cjs +0 -237
- package/dist/server-multipart/index.cjs.map +0 -1
- package/dist/server-multipart/index.d.cts +0 -50
- package/dist/server-proxy/index.cjs +0 -186
- package/dist/server-proxy/index.cjs.map +0 -1
- package/dist/server-proxy/index.d.cts +0 -234
- package/dist/server-rate-limit/index.cjs +0 -241
- package/dist/server-rate-limit/index.cjs.map +0 -1
- package/dist/server-rate-limit/index.d.cts +0 -183
- package/dist/server-security/index.cjs +0 -316
- package/dist/server-security/index.cjs.map +0 -1
- package/dist/server-security/index.d.cts +0 -173
- package/dist/server-static/index.cjs +0 -170
- package/dist/server-static/index.cjs.map +0 -1
- package/dist/server-static/index.d.cts +0 -121
- package/dist/server-swagger/index.cjs +0 -1021
- package/dist/server-swagger/index.cjs.map +0 -1
- package/dist/server-swagger/index.d.cts +0 -382
- package/dist/sms/index.cjs +0 -221
- package/dist/sms/index.cjs.map +0 -1
- package/dist/sms/index.d.cts +0 -130
- package/dist/thread/index.cjs +0 -350
- package/dist/thread/index.cjs.map +0 -1
- package/dist/thread/index.d.cts +0 -260
- package/dist/topic/index.cjs +0 -282
- package/dist/topic/index.cjs.map +0 -1
- package/dist/topic/index.d.cts +0 -523
- package/dist/topic-redis/index.cjs +0 -71
- package/dist/topic-redis/index.cjs.map +0 -1
- package/dist/topic-redis/index.d.cts +0 -42
- package/dist/vite/index.cjs +0 -1077
- package/dist/vite/index.cjs.map +0 -1
- package/dist/vite/index.d.cts +0 -542
- package/dist/websocket/index.cjs +0 -1117
- package/dist/websocket/index.cjs.map +0 -1
- package/dist/websocket/index.d.cts +0 -861
package/dist/websocket/index.cjs
DELETED
|
@@ -1,1117 +0,0 @@
|
|
|
1
|
-
let alepha = require("alepha");
|
|
2
|
-
let alepha_server = require("alepha/server");
|
|
3
|
-
let alepha_topic = require("alepha/topic");
|
|
4
|
-
let alepha_logger = require("alepha/logger");
|
|
5
|
-
let ws = require("ws");
|
|
6
|
-
|
|
7
|
-
//#region src/websocket/descriptors/$channel.ts
|
|
8
|
-
/**
|
|
9
|
-
* Defines a WebSocket channel with specified client and server message schemas.
|
|
10
|
-
*
|
|
11
|
-
* Channels must be defined as class properties to be registered in the Alepha context.
|
|
12
|
-
* They define the "vocabulary" for communication - the schema for messages flowing
|
|
13
|
-
* in both directions (server→client and client→server).
|
|
14
|
-
*
|
|
15
|
-
* @example Server-side with $websocket
|
|
16
|
-
* ```typescript
|
|
17
|
-
* class ChatController {
|
|
18
|
-
* // Channel must be defined inside a class
|
|
19
|
-
* chatChannel = $channel({
|
|
20
|
-
* path: "/ws/chat",
|
|
21
|
-
* description: "Real-time chat channel",
|
|
22
|
-
* schema: {
|
|
23
|
-
* // Server → Client messages
|
|
24
|
-
* in: t.union([
|
|
25
|
-
* t.object({
|
|
26
|
-
* type: t.const("append"),
|
|
27
|
-
* content: t.text(),
|
|
28
|
-
* username: t.text()
|
|
29
|
-
* }),
|
|
30
|
-
* t.object({
|
|
31
|
-
* type: t.const("system"),
|
|
32
|
-
* message: t.text()
|
|
33
|
-
* })
|
|
34
|
-
* ]),
|
|
35
|
-
* // Client → Server messages
|
|
36
|
-
* out: t.object({
|
|
37
|
-
* content: t.text()
|
|
38
|
-
* })
|
|
39
|
-
* }
|
|
40
|
-
* });
|
|
41
|
-
*
|
|
42
|
-
* chat = $websocket({
|
|
43
|
-
* channel: this.chatChannel,
|
|
44
|
-
* handler: async ({ message, reply }) => {
|
|
45
|
-
* await reply({
|
|
46
|
-
* message: { type: "append", content: message.content, username: "user" }
|
|
47
|
-
* });
|
|
48
|
-
* }
|
|
49
|
-
* });
|
|
50
|
-
* }
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* @example Browser-side with useRoom
|
|
54
|
-
* ```typescript
|
|
55
|
-
* // Define channel in a class for browser context
|
|
56
|
-
* class ChatClient {
|
|
57
|
-
* chatChannel = $channel({
|
|
58
|
-
* path: "/ws/chat",
|
|
59
|
-
* schema: { in: inSchema, out: outSchema }
|
|
60
|
-
* });
|
|
61
|
-
* }
|
|
62
|
-
*
|
|
63
|
-
* // Use in React component
|
|
64
|
-
* function Chat() {
|
|
65
|
-
* const client = useInject(ChatClient);
|
|
66
|
-
* const chat = useRoom({ roomId: "lobby", channel: client.chatChannel, handler: ... }, []);
|
|
67
|
-
* }
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
const $channel = (options) => {
|
|
71
|
-
return (0, alepha.createDescriptor)(ChannelDescriptor, options);
|
|
72
|
-
};
|
|
73
|
-
var ChannelDescriptor = class extends alepha.Descriptor {};
|
|
74
|
-
$channel[alepha.KIND] = ChannelDescriptor;
|
|
75
|
-
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/websocket/providers/WebSocketServerProvider.ts
|
|
78
|
-
/**
|
|
79
|
-
* Abstract WebSocket server provider
|
|
80
|
-
*
|
|
81
|
-
* This class provides the base interface that must be implemented by
|
|
82
|
-
* platform-specific providers (Node.js, Browser, etc.)
|
|
83
|
-
*/
|
|
84
|
-
var WebSocketServerProvider = class {};
|
|
85
|
-
|
|
86
|
-
//#endregion
|
|
87
|
-
//#region src/websocket/descriptors/$websocket.ts
|
|
88
|
-
/**
|
|
89
|
-
* Defines a WebSocket server endpoint for a specific channel.
|
|
90
|
-
*
|
|
91
|
-
* Server-side only. Creates a WebSocket endpoint that:
|
|
92
|
-
* - Accepts connections from clients
|
|
93
|
-
* - Validates incoming messages against the channel schema
|
|
94
|
-
* - Provides room-based messaging
|
|
95
|
-
* - Integrates with alepha/security for authentication (optional)
|
|
96
|
-
* - Supports horizontal scaling via alepha/topic
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```typescript
|
|
100
|
-
* class ChatController {
|
|
101
|
-
* chat = $websocket({
|
|
102
|
-
* channel: chatChannel,
|
|
103
|
-
* handler: async ({ connectionId, userId, roomId, message, reply }) => {
|
|
104
|
-
* // Broadcast to all in room except sender
|
|
105
|
-
* await reply({
|
|
106
|
-
* message: {
|
|
107
|
-
* type: "append",
|
|
108
|
-
* username: userId,
|
|
109
|
-
* content: message.content
|
|
110
|
-
* },
|
|
111
|
-
* exceptSelf: true
|
|
112
|
-
* });
|
|
113
|
-
* }
|
|
114
|
-
* });
|
|
115
|
-
*
|
|
116
|
-
* async broadcastAnnouncement(roomId: string, text: string) {
|
|
117
|
-
* await this.chat.emit({
|
|
118
|
-
* roomId,
|
|
119
|
-
* message: {
|
|
120
|
-
* type: "append",
|
|
121
|
-
* username: "System",
|
|
122
|
-
* content: text
|
|
123
|
-
* }
|
|
124
|
-
* });
|
|
125
|
-
* }
|
|
126
|
-
* }
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
const $websocket = (options) => {
|
|
130
|
-
return (0, alepha.createDescriptor)(WebSocketDescriptor, options);
|
|
131
|
-
};
|
|
132
|
-
var WebSocketDescriptor = class extends alepha.Descriptor {
|
|
133
|
-
webSocketServerProvider = (0, alepha.$inject)(WebSocketServerProvider);
|
|
134
|
-
onInit() {
|
|
135
|
-
this.webSocketServerProvider.registerEndpoint(this.options);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Emit message to clients
|
|
139
|
-
*
|
|
140
|
-
* Send messages from the server to connected clients based on targeting criteria.
|
|
141
|
-
* Messages are distributed across all server instances via pub/sub.
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```typescript
|
|
145
|
-
* // Send to specific room
|
|
146
|
-
* await websocket.emit({
|
|
147
|
-
* roomId: "room-123",
|
|
148
|
-
* message: { type: "update", data: {...} }
|
|
149
|
-
* });
|
|
150
|
-
*
|
|
151
|
-
* // Send to specific user (all their connections)
|
|
152
|
-
* await websocket.emit({
|
|
153
|
-
* userId: "user-456",
|
|
154
|
-
* message: { type: "notification", text: "Hello!" }
|
|
155
|
-
* });
|
|
156
|
-
*
|
|
157
|
-
* // Send to multiple rooms, except certain users
|
|
158
|
-
* await websocket.emit({
|
|
159
|
-
* roomIds: ["room-1", "room-2"],
|
|
160
|
-
* exceptUserIds: ["user-123"],
|
|
161
|
-
* message: { type: "broadcast", content: "System announcement" }
|
|
162
|
-
* });
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
async emit(options) {
|
|
166
|
-
await this.webSocketServerProvider.emit(this.options.channel.options.path, options);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
$websocket[alepha.KIND] = WebSocketDescriptor;
|
|
170
|
-
|
|
171
|
-
//#endregion
|
|
172
|
-
//#region src/websocket/errors/WebSocketError.ts
|
|
173
|
-
/**
|
|
174
|
-
* Base WebSocket error class
|
|
175
|
-
*/
|
|
176
|
-
var WebSocketError = class extends Error {
|
|
177
|
-
constructor(message, code) {
|
|
178
|
-
super(message);
|
|
179
|
-
this.code = code;
|
|
180
|
-
this.name = "WebSocketError";
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
/**
|
|
184
|
-
* Error thrown when WebSocket connection fails
|
|
185
|
-
*/
|
|
186
|
-
var WebSocketConnectionError = class extends WebSocketError {
|
|
187
|
-
constructor(message, code) {
|
|
188
|
-
super(message, code);
|
|
189
|
-
this.name = "WebSocketConnectionError";
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Error thrown when WebSocket message validation fails
|
|
194
|
-
*/
|
|
195
|
-
var WebSocketValidationError = class extends WebSocketError {
|
|
196
|
-
constructor(message) {
|
|
197
|
-
super(message);
|
|
198
|
-
this.name = "WebSocketValidationError";
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
//#endregion
|
|
203
|
-
//#region src/websocket/services/RoomManager.ts
|
|
204
|
-
/**
|
|
205
|
-
* Manages WebSocket room memberships
|
|
206
|
-
*
|
|
207
|
-
* Rooms are logical groupings of connections. A connection can be in multiple rooms,
|
|
208
|
-
* and messages can be targeted to specific rooms.
|
|
209
|
-
*/
|
|
210
|
-
var RoomManager = class {
|
|
211
|
-
log = (0, alepha_logger.$logger)();
|
|
212
|
-
/**
|
|
213
|
-
* Maps roomId → Set<connectionId>
|
|
214
|
-
*/
|
|
215
|
-
rooms = /* @__PURE__ */ new Map();
|
|
216
|
-
/**
|
|
217
|
-
* Maps connectionId → Set<roomId>
|
|
218
|
-
* Inverse index for fast lookup of connection's rooms
|
|
219
|
-
*/
|
|
220
|
-
connectionRooms = /* @__PURE__ */ new Map();
|
|
221
|
-
/**
|
|
222
|
-
* Join a connection to one or more rooms
|
|
223
|
-
*/
|
|
224
|
-
joinRooms(connectionId, roomIds) {
|
|
225
|
-
for (const roomId of roomIds) this.joinRoom(connectionId, roomId);
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Join a connection to a room
|
|
229
|
-
*/
|
|
230
|
-
joinRoom(connectionId, roomId) {
|
|
231
|
-
let room = this.rooms.get(roomId);
|
|
232
|
-
if (!room) {
|
|
233
|
-
room = /* @__PURE__ */ new Set();
|
|
234
|
-
this.rooms.set(roomId, room);
|
|
235
|
-
}
|
|
236
|
-
room.add(connectionId);
|
|
237
|
-
let connRooms = this.connectionRooms.get(connectionId);
|
|
238
|
-
if (!connRooms) {
|
|
239
|
-
connRooms = /* @__PURE__ */ new Set();
|
|
240
|
-
this.connectionRooms.set(connectionId, connRooms);
|
|
241
|
-
}
|
|
242
|
-
connRooms.add(roomId);
|
|
243
|
-
this.log.debug(`Connection ${connectionId} joined room ${roomId}`);
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Leave a connection from a room
|
|
247
|
-
*/
|
|
248
|
-
leaveRoom(connectionId, roomId) {
|
|
249
|
-
const room = this.rooms.get(roomId);
|
|
250
|
-
if (room) {
|
|
251
|
-
room.delete(connectionId);
|
|
252
|
-
if (room.size === 0) this.rooms.delete(roomId);
|
|
253
|
-
}
|
|
254
|
-
const connRooms = this.connectionRooms.get(connectionId);
|
|
255
|
-
if (connRooms) {
|
|
256
|
-
connRooms.delete(roomId);
|
|
257
|
-
if (connRooms.size === 0) this.connectionRooms.delete(connectionId);
|
|
258
|
-
}
|
|
259
|
-
this.log.debug(`Connection ${connectionId} left room ${roomId}`);
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Remove a connection from all rooms
|
|
263
|
-
*/
|
|
264
|
-
leaveAllRooms(connectionId) {
|
|
265
|
-
const connRooms = this.connectionRooms.get(connectionId);
|
|
266
|
-
if (!connRooms) return;
|
|
267
|
-
for (const roomId of connRooms) {
|
|
268
|
-
const room = this.rooms.get(roomId);
|
|
269
|
-
if (room) {
|
|
270
|
-
room.delete(connectionId);
|
|
271
|
-
if (room.size === 0) this.rooms.delete(roomId);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
this.connectionRooms.delete(connectionId);
|
|
275
|
-
this.log.debug(`Connection ${connectionId} left all rooms`);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Get all connection IDs in a room
|
|
279
|
-
*/
|
|
280
|
-
getRoomConnections(roomId) {
|
|
281
|
-
const room = this.rooms.get(roomId);
|
|
282
|
-
return room ? Array.from(room) : [];
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Get all room IDs for a connection
|
|
286
|
-
*/
|
|
287
|
-
getConnectionRooms(connectionId) {
|
|
288
|
-
const connRooms = this.connectionRooms.get(connectionId);
|
|
289
|
-
return connRooms ? Array.from(connRooms) : [];
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Check if a connection is in a room
|
|
293
|
-
*/
|
|
294
|
-
isInRoom(connectionId, roomId) {
|
|
295
|
-
const connRooms = this.connectionRooms.get(connectionId);
|
|
296
|
-
return connRooms ? connRooms.has(roomId) : false;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Get all active rooms
|
|
300
|
-
*/
|
|
301
|
-
getAllRooms() {
|
|
302
|
-
return Array.from(this.rooms.keys());
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Get total number of connections across all rooms
|
|
306
|
-
*/
|
|
307
|
-
getTotalConnections() {
|
|
308
|
-
return this.connectionRooms.size;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Get room statistics
|
|
312
|
-
*/
|
|
313
|
-
getStats() {
|
|
314
|
-
const roomSizes = /* @__PURE__ */ new Map();
|
|
315
|
-
for (const [roomId, connections] of this.rooms) roomSizes.set(roomId, connections.size);
|
|
316
|
-
return {
|
|
317
|
-
totalRooms: this.rooms.size,
|
|
318
|
-
totalConnections: this.connectionRooms.size,
|
|
319
|
-
roomSizes
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
//#endregion
|
|
325
|
-
//#region src/websocket/services/WebSocketTopicService.ts
|
|
326
|
-
/**
|
|
327
|
-
* WebSocket message distribution event
|
|
328
|
-
*/
|
|
329
|
-
const webSocketMessageSchema = { payload: alepha.t.object({
|
|
330
|
-
channelPath: alepha.t.text(),
|
|
331
|
-
roomIds: alepha.t.optional(alepha.t.array(alepha.t.text())),
|
|
332
|
-
userIds: alepha.t.optional(alepha.t.array(alepha.t.text())),
|
|
333
|
-
connectionIds: alepha.t.optional(alepha.t.array(alepha.t.text())),
|
|
334
|
-
exceptConnectionIds: alepha.t.optional(alepha.t.array(alepha.t.text())),
|
|
335
|
-
exceptUserIds: alepha.t.optional(alepha.t.array(alepha.t.text())),
|
|
336
|
-
message: alepha.t.any()
|
|
337
|
-
}) };
|
|
338
|
-
/**
|
|
339
|
-
* WebSocket Topic Service
|
|
340
|
-
*
|
|
341
|
-
* Manages pub/sub messaging for WebSocket connections across multiple server instances.
|
|
342
|
-
* Uses alepha/topic for cross-instance message distribution, enabling horizontal scaling.
|
|
343
|
-
*
|
|
344
|
-
* When a WebSocket message needs to be sent:
|
|
345
|
-
* 1. Server instance A publishes to the topic
|
|
346
|
-
* 2. All server instances (A, B, C, etc.) receive the message
|
|
347
|
-
* 3. Each instance sends to its local connections that match the criteria
|
|
348
|
-
*
|
|
349
|
-
* This enables:
|
|
350
|
-
* - Multiple server instances handling WebSocket connections
|
|
351
|
-
* - Redis-backed message distribution (with alepha/topic/redis)
|
|
352
|
-
* - Horizontal scaling without losing messages
|
|
353
|
-
*/
|
|
354
|
-
var WebSocketTopicService = class {
|
|
355
|
-
log = (0, alepha_logger.$logger)();
|
|
356
|
-
/**
|
|
357
|
-
* Handler function to be called when a message is received from the topic
|
|
358
|
-
* This is set by the WebSocket provider during initialization
|
|
359
|
-
*/
|
|
360
|
-
messageHandler;
|
|
361
|
-
/**
|
|
362
|
-
* Topic for distributing WebSocket messages across server instances
|
|
363
|
-
*/
|
|
364
|
-
topic = (0, alepha_topic.$topic)({
|
|
365
|
-
name: "websocket:broadcast",
|
|
366
|
-
description: "Distributes WebSocket messages across server instances for horizontal scaling",
|
|
367
|
-
schema: webSocketMessageSchema,
|
|
368
|
-
handler: async (message) => {
|
|
369
|
-
if (this.messageHandler) await this.messageHandler(message.payload);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
/**
|
|
373
|
-
* Publish a message to be distributed across all server instances
|
|
374
|
-
*/
|
|
375
|
-
async publish(event) {
|
|
376
|
-
await this.topic.publish(event);
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Set the handler for incoming messages
|
|
380
|
-
*/
|
|
381
|
-
setMessageHandler(handler) {
|
|
382
|
-
this.messageHandler = handler;
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/websocket/providers/NodeWebSocketServerProvider.ts
|
|
388
|
-
const envSchema$1 = alepha.t.object({ WEBSOCKET_PATH: alepha.t.text({
|
|
389
|
-
default: "/ws",
|
|
390
|
-
description: "Base path for WebSocket endpoints"
|
|
391
|
-
}) });
|
|
392
|
-
var NodeWebSocketServerProvider = class extends WebSocketServerProvider {
|
|
393
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
394
|
-
roomManager = (0, alepha.$inject)(RoomManager);
|
|
395
|
-
topicService = (0, alepha.$inject)(WebSocketTopicService);
|
|
396
|
-
log = (0, alepha_logger.$logger)();
|
|
397
|
-
env = (0, alepha.$env)(envSchema$1);
|
|
398
|
-
wss;
|
|
399
|
-
endpoints = /* @__PURE__ */ new Map();
|
|
400
|
-
connections = /* @__PURE__ */ new Map();
|
|
401
|
-
userConnections = /* @__PURE__ */ new Map();
|
|
402
|
-
nextConnectionId = 1;
|
|
403
|
-
registerEndpoint(config) {
|
|
404
|
-
const path = config.channel.options.path;
|
|
405
|
-
this.endpoints.set(path, config);
|
|
406
|
-
}
|
|
407
|
-
async emit(channelPath, options) {
|
|
408
|
-
await this.topicService.publish({
|
|
409
|
-
channelPath,
|
|
410
|
-
roomIds: options.roomIds ? options.roomIds : options.roomId ? [options.roomId] : void 0,
|
|
411
|
-
userIds: options.userIds ? options.userIds : options.userId ? [options.userId] : void 0,
|
|
412
|
-
connectionIds: options.connectionIds ? options.connectionIds : options.connectionId ? [options.connectionId] : void 0,
|
|
413
|
-
exceptConnectionIds: options.exceptConnectionIds,
|
|
414
|
-
exceptUserIds: options.exceptUserIds,
|
|
415
|
-
message: options.message
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
getConnections() {
|
|
419
|
-
return Array.from(this.connections.values());
|
|
420
|
-
}
|
|
421
|
-
getRoomConnections(roomId) {
|
|
422
|
-
return this.roomManager.getRoomConnections(roomId).map((id) => this.connections.get(id)).filter((conn) => conn !== void 0);
|
|
423
|
-
}
|
|
424
|
-
getUserConnections(userId) {
|
|
425
|
-
const connectionIds = this.userConnections.get(userId);
|
|
426
|
-
if (!connectionIds) return [];
|
|
427
|
-
return Array.from(connectionIds).map((id) => this.connections.get(id)).filter((conn) => conn !== void 0);
|
|
428
|
-
}
|
|
429
|
-
async closeConnection(connectionId, code, reason) {
|
|
430
|
-
const connection = this.connections.get(connectionId);
|
|
431
|
-
if (!connection) {
|
|
432
|
-
this.log.warn(`Connection not found: ${connectionId}`);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
await connection.close(code, reason);
|
|
436
|
-
}
|
|
437
|
-
handleUpgrade(request, socket, head) {
|
|
438
|
-
const path = new URL(request.url || "/", "http://localhost").pathname;
|
|
439
|
-
const endpoint = this.endpoints.get(path);
|
|
440
|
-
if (!endpoint) {
|
|
441
|
-
if (!this.alepha.isViteDev()) {
|
|
442
|
-
this.log.warn(`No WebSocket endpoint found for path: ${path}`);
|
|
443
|
-
socket.destroy();
|
|
444
|
-
}
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
this.log.debug(`WebSocket upgrade request: ${path}`);
|
|
448
|
-
this.wss?.handleUpgrade(request, socket, head, (ws$1) => {
|
|
449
|
-
this.handleConnection(ws$1, endpoint, request);
|
|
450
|
-
});
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
handleConnection(ws$1, endpoint, request) {
|
|
454
|
-
const connectionId = `ws-${this.nextConnectionId++}`;
|
|
455
|
-
const userId = void 0;
|
|
456
|
-
const url = new URL(request.url || "/", "http://localhost");
|
|
457
|
-
const roomIds = this.extractRoomIds(url);
|
|
458
|
-
const connection = this.alepha.inject(NodeWebSocketConnection, {
|
|
459
|
-
lifetime: "transient",
|
|
460
|
-
args: [
|
|
461
|
-
connectionId,
|
|
462
|
-
userId,
|
|
463
|
-
roomIds,
|
|
464
|
-
ws$1,
|
|
465
|
-
this,
|
|
466
|
-
endpoint
|
|
467
|
-
]
|
|
468
|
-
});
|
|
469
|
-
this.connections.set(connectionId, connection);
|
|
470
|
-
if (roomIds.length > 0) this.roomManager.joinRooms(connectionId, roomIds);
|
|
471
|
-
this.log.info(`WebSocket connection established: ${connectionId}`, {
|
|
472
|
-
path: endpoint.channel.options.path,
|
|
473
|
-
userId,
|
|
474
|
-
roomIds,
|
|
475
|
-
remoteAddress: request.socket.remoteAddress
|
|
476
|
-
});
|
|
477
|
-
if (endpoint.onConnect) Promise.resolve(endpoint.onConnect({
|
|
478
|
-
connectionId,
|
|
479
|
-
userId,
|
|
480
|
-
roomIds
|
|
481
|
-
})).catch((error) => {
|
|
482
|
-
this.log.error("Error in onConnect handler:", error);
|
|
483
|
-
});
|
|
484
|
-
ws$1.on("message", async (data) => {
|
|
485
|
-
await connection.handleMessage(data);
|
|
486
|
-
});
|
|
487
|
-
ws$1.on("close", (code, reason) => {
|
|
488
|
-
this.log.info(`WebSocket connection closed: ${connectionId}`, {
|
|
489
|
-
code,
|
|
490
|
-
reason: reason.toString()
|
|
491
|
-
});
|
|
492
|
-
this.connections.delete(connectionId);
|
|
493
|
-
this.roomManager.leaveAllRooms(connectionId);
|
|
494
|
-
if (endpoint.onDisconnect) Promise.resolve(endpoint.onDisconnect({
|
|
495
|
-
connectionId,
|
|
496
|
-
userId,
|
|
497
|
-
roomIds
|
|
498
|
-
})).catch((error) => {
|
|
499
|
-
this.log.error("Error in onDisconnect handler:", error);
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
ws$1.on("error", (error) => {
|
|
503
|
-
this.log.error(`WebSocket error on ${connectionId}:`, error);
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
extractRoomIds(url) {
|
|
507
|
-
const roomIds = [];
|
|
508
|
-
const roomIdParams = url.searchParams.getAll("roomId");
|
|
509
|
-
roomIds.push(...roomIdParams);
|
|
510
|
-
const roomIdsParam = url.searchParams.get("roomIds");
|
|
511
|
-
if (roomIdsParam) roomIds.push(...roomIdsParam.split(",").map((id) => id.trim()));
|
|
512
|
-
if (roomIds.length === 0) roomIds.push("default");
|
|
513
|
-
return roomIds;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Send message to local connections based on targeting criteria
|
|
517
|
-
* This is called by the topic service when a message is received
|
|
518
|
-
*/
|
|
519
|
-
async sendToLocalConnections(channelPath, message, criteria) {
|
|
520
|
-
const targetConnections = /* @__PURE__ */ new Set();
|
|
521
|
-
if (criteria.roomIds) for (const roomId of criteria.roomIds) {
|
|
522
|
-
const roomConns = this.roomManager.getRoomConnections(roomId);
|
|
523
|
-
for (const connId of roomConns) targetConnections.add(connId);
|
|
524
|
-
}
|
|
525
|
-
if (criteria.userIds) for (const userId of criteria.userIds) {
|
|
526
|
-
const userConns = this.userConnections.get(userId);
|
|
527
|
-
if (userConns) for (const connId of userConns) targetConnections.add(connId);
|
|
528
|
-
}
|
|
529
|
-
if (criteria.connectionIds) for (const connId of criteria.connectionIds) targetConnections.add(connId);
|
|
530
|
-
if (!criteria.roomIds && !criteria.userIds && !criteria.connectionIds) for (const conn of this.connections.values()) targetConnections.add(conn.id);
|
|
531
|
-
if (criteria.exceptConnectionIds) for (const connId of criteria.exceptConnectionIds) targetConnections.delete(connId);
|
|
532
|
-
if (criteria.exceptUserIds) for (const userId of criteria.exceptUserIds) {
|
|
533
|
-
const userConns = this.userConnections.get(userId);
|
|
534
|
-
if (userConns) for (const connId of userConns) targetConnections.delete(connId);
|
|
535
|
-
}
|
|
536
|
-
const serialized = JSON.stringify(message);
|
|
537
|
-
await Promise.all(Array.from(targetConnections).map(async (connId) => {
|
|
538
|
-
const conn = this.connections.get(connId);
|
|
539
|
-
if (conn) try {
|
|
540
|
-
await conn.send(serialized);
|
|
541
|
-
} catch (error) {
|
|
542
|
-
this.log.error(`Failed to send to connection ${connId}:`, error);
|
|
543
|
-
}
|
|
544
|
-
}));
|
|
545
|
-
}
|
|
546
|
-
start = (0, alepha.$hook)({
|
|
547
|
-
on: "start",
|
|
548
|
-
handler: async () => {
|
|
549
|
-
if (this.alepha.isServerless()) {
|
|
550
|
-
this.log.debug("WebSocket server disabled in serverless mode");
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
this.wss = new ws.WebSocketServer({ noServer: true });
|
|
554
|
-
for (const [path, endpoint] of this.endpoints.entries()) this.log.debug(`WebSocket endpoint registered: ${path}`);
|
|
555
|
-
this.topicService.setMessageHandler(async (event) => {
|
|
556
|
-
await this.sendToLocalConnections(event.channelPath, event.message, {
|
|
557
|
-
roomIds: event.roomIds,
|
|
558
|
-
userIds: event.userIds,
|
|
559
|
-
connectionIds: event.connectionIds,
|
|
560
|
-
exceptConnectionIds: event.exceptConnectionIds,
|
|
561
|
-
exceptUserIds: event.exceptUserIds
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
this.log.info("WebSocket server OK", { basePath: this.env.WEBSOCKET_PATH });
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
ready = (0, alepha.$hook)({
|
|
568
|
-
on: "ready",
|
|
569
|
-
handler: async () => {
|
|
570
|
-
if (this.alepha.isServerless() || !this.wss) return;
|
|
571
|
-
const httpServer = this.alepha.state.get("alepha.node.server");
|
|
572
|
-
if (httpServer) {
|
|
573
|
-
httpServer.on("upgrade", (request, socket, head) => {
|
|
574
|
-
this.handleUpgrade(request, socket, head);
|
|
575
|
-
});
|
|
576
|
-
this.log.debug("WebSocket upgrade handler attached to HTTP server");
|
|
577
|
-
} else this.log.warn("No HTTP server found - WebSocket upgrade handler not attached");
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
stop = (0, alepha.$hook)({
|
|
581
|
-
on: "stop",
|
|
582
|
-
handler: async () => {
|
|
583
|
-
if (!this.wss) return;
|
|
584
|
-
for (const connection of this.connections.values()) await connection.close(1001, "Server shutting down");
|
|
585
|
-
await new Promise((resolve, reject) => {
|
|
586
|
-
this.wss?.close((err) => {
|
|
587
|
-
if (err) reject(err);
|
|
588
|
-
else resolve();
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
this.log.info("WebSocket server closed");
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
};
|
|
595
|
-
var NodeWebSocketConnection = class {
|
|
596
|
-
log = (0, alepha_logger.$logger)();
|
|
597
|
-
metadata;
|
|
598
|
-
constructor(id, userId, roomIds, ws$1, provider, endpoint) {
|
|
599
|
-
this.id = id;
|
|
600
|
-
this.userId = userId;
|
|
601
|
-
this.roomIds = roomIds;
|
|
602
|
-
this.ws = ws$1;
|
|
603
|
-
this.provider = provider;
|
|
604
|
-
this.endpoint = endpoint;
|
|
605
|
-
}
|
|
606
|
-
get readyState() {
|
|
607
|
-
return this.ws.readyState;
|
|
608
|
-
}
|
|
609
|
-
async send(message) {
|
|
610
|
-
if (this.ws.readyState !== ws.WebSocket.OPEN) throw new Error("WebSocket is not open");
|
|
611
|
-
const data = typeof message === "string" ? message : JSON.stringify(message);
|
|
612
|
-
await new Promise((resolve, reject) => {
|
|
613
|
-
this.ws.send(data, (err) => {
|
|
614
|
-
if (err) reject(err);
|
|
615
|
-
else resolve();
|
|
616
|
-
});
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
async close(code, reason) {
|
|
620
|
-
this.ws.close(code, reason);
|
|
621
|
-
}
|
|
622
|
-
async handleMessage(data) {
|
|
623
|
-
try {
|
|
624
|
-
const rawMessage = data.toString();
|
|
625
|
-
let parsed;
|
|
626
|
-
try {
|
|
627
|
-
parsed = JSON.parse(rawMessage);
|
|
628
|
-
} catch {
|
|
629
|
-
this.log.warn("Received non-JSON message");
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
const roomId = parsed.roomId || this.roomIds[0] || "default";
|
|
633
|
-
const message = parsed.message || parsed;
|
|
634
|
-
const outSchema = this.endpoint.channel.options.schema.out;
|
|
635
|
-
if (!alepha.TypeBoxValue.Check(outSchema, message)) throw new WebSocketValidationError(`Message validation failed: ${Array.from(alepha.TypeBoxValue.Errors(outSchema, message)).map((e) => e.message).join(", ")}`);
|
|
636
|
-
const reply = async (options) => {
|
|
637
|
-
const targetRoomId = options.roomId || roomId;
|
|
638
|
-
const exceptConnectionIds = options.exceptConnectionIds || [];
|
|
639
|
-
if (options.exceptSelf) exceptConnectionIds.push(this.id);
|
|
640
|
-
await this.provider.emit(this.endpoint.channel.options.path, {
|
|
641
|
-
message: options.message,
|
|
642
|
-
roomId: targetRoomId,
|
|
643
|
-
exceptConnectionIds,
|
|
644
|
-
exceptUserIds: options.exceptUserIds
|
|
645
|
-
});
|
|
646
|
-
};
|
|
647
|
-
const context = {
|
|
648
|
-
connectionId: this.id,
|
|
649
|
-
userId: this.userId,
|
|
650
|
-
roomId,
|
|
651
|
-
message,
|
|
652
|
-
reply
|
|
653
|
-
};
|
|
654
|
-
await this.endpoint.handler(context);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
this.log.error(`Error handling WebSocket message on ${this.id}:`, error);
|
|
657
|
-
await this.send({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
//#endregion
|
|
663
|
-
//#region src/websocket/interfaces/WebSocketInterfaces.ts
|
|
664
|
-
/**
|
|
665
|
-
* WebSocket state enum
|
|
666
|
-
*/
|
|
667
|
-
let WebSocketState = /* @__PURE__ */ function(WebSocketState$1) {
|
|
668
|
-
WebSocketState$1[WebSocketState$1["CONNECTING"] = 0] = "CONNECTING";
|
|
669
|
-
WebSocketState$1[WebSocketState$1["OPEN"] = 1] = "OPEN";
|
|
670
|
-
WebSocketState$1[WebSocketState$1["CLOSING"] = 2] = "CLOSING";
|
|
671
|
-
WebSocketState$1[WebSocketState$1["CLOSED"] = 3] = "CLOSED";
|
|
672
|
-
return WebSocketState$1;
|
|
673
|
-
}({});
|
|
674
|
-
|
|
675
|
-
//#endregion
|
|
676
|
-
//#region src/websocket/services/WebSocketClient.ts
|
|
677
|
-
const envSchema = alepha.t.object({
|
|
678
|
-
WEBSOCKET_URL: alepha.t.text({
|
|
679
|
-
default: "",
|
|
680
|
-
description: "WebSocket server URL (e.g., ws://localhost:3001). Leave empty to auto-detect."
|
|
681
|
-
}),
|
|
682
|
-
WEBSOCKET_RECONNECT_INTERVAL: alepha.t.integer({
|
|
683
|
-
default: 3e3,
|
|
684
|
-
description: "Reconnection interval in milliseconds"
|
|
685
|
-
}),
|
|
686
|
-
WEBSOCKET_MAX_RECONNECT_ATTEMPTS: alepha.t.integer({
|
|
687
|
-
default: 10,
|
|
688
|
-
description: "Maximum number of reconnection attempts. Set to -1 for infinite."
|
|
689
|
-
})
|
|
690
|
-
});
|
|
691
|
-
/**
|
|
692
|
-
* WebSocket channel connection
|
|
693
|
-
*
|
|
694
|
-
* Manages a single WebSocket connection to a channel with multiple room subscriptions.
|
|
695
|
-
* One connection can handle multiple rooms on the same channel.
|
|
696
|
-
*/
|
|
697
|
-
var WebSocketChannelConnection = class {
|
|
698
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
699
|
-
log = (0, alepha_logger.$logger)();
|
|
700
|
-
ws;
|
|
701
|
-
reconnectAttempts = 0;
|
|
702
|
-
reconnectTimer;
|
|
703
|
-
messageQueue = [];
|
|
704
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
705
|
-
isConnected = false;
|
|
706
|
-
isConnecting = false;
|
|
707
|
-
isError = false;
|
|
708
|
-
error;
|
|
709
|
-
onConnectCallbacks = /* @__PURE__ */ new Set();
|
|
710
|
-
onDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
711
|
-
onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
712
|
-
constructor(channel, options, env) {
|
|
713
|
-
this.channel = channel;
|
|
714
|
-
this.options = options;
|
|
715
|
-
this.env = env;
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* Build WebSocket URL
|
|
719
|
-
*/
|
|
720
|
-
buildUrl() {
|
|
721
|
-
this.log.trace("Building WebSocket URL", {
|
|
722
|
-
hasCustomUrl: !!this.options.url,
|
|
723
|
-
channelPath: this.channel.options.path
|
|
724
|
-
});
|
|
725
|
-
if (this.options.url) {
|
|
726
|
-
this.log.debug("Using custom WebSocket URL", { url: this.options.url });
|
|
727
|
-
return this.options.url;
|
|
728
|
-
}
|
|
729
|
-
if (typeof window !== "undefined") {
|
|
730
|
-
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
731
|
-
const host = window.location.host;
|
|
732
|
-
const path = this.channel.options.path;
|
|
733
|
-
const roomIds = Array.from(this.subscriptions.keys());
|
|
734
|
-
const url$1 = `${protocol}//${host}${path}${roomIds.length > 0 ? `?roomIds=${roomIds.join(",")}` : ""}`;
|
|
735
|
-
this.log.debug("Auto-detected WebSocket URL", {
|
|
736
|
-
url: url$1,
|
|
737
|
-
roomIds
|
|
738
|
-
});
|
|
739
|
-
return url$1;
|
|
740
|
-
}
|
|
741
|
-
const url = `${this.env.WEBSOCKET_URL}${this.channel.options.path}`;
|
|
742
|
-
this.log.debug("Using env WebSocket URL", { url });
|
|
743
|
-
return url;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Subscribe to a room on this channel
|
|
747
|
-
*/
|
|
748
|
-
subscribe(roomId, handler, callbacks) {
|
|
749
|
-
this.log.debug("Subscribing to room", {
|
|
750
|
-
roomId,
|
|
751
|
-
channelPath: this.channel.options.path,
|
|
752
|
-
existingSubscriptions: this.subscriptions.size
|
|
753
|
-
});
|
|
754
|
-
this.subscriptions.set(roomId, handler);
|
|
755
|
-
if (callbacks?.onConnect) this.onConnectCallbacks.add(callbacks.onConnect);
|
|
756
|
-
if (callbacks?.onDisconnect) this.onDisconnectCallbacks.add(callbacks.onDisconnect);
|
|
757
|
-
if (callbacks?.onError) this.onErrorCallbacks.add(callbacks.onError);
|
|
758
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
759
|
-
this.log.trace("No active connection, initiating connect");
|
|
760
|
-
this.connect().catch((error) => {
|
|
761
|
-
this.log.error("Failed to connect:", error);
|
|
762
|
-
});
|
|
763
|
-
} else this.log.trace("Already connected, reusing existing connection");
|
|
764
|
-
return () => {
|
|
765
|
-
this.log.debug("Unsubscribing from room", { roomId });
|
|
766
|
-
this.subscriptions.delete(roomId);
|
|
767
|
-
if (callbacks?.onConnect) this.onConnectCallbacks.delete(callbacks.onConnect);
|
|
768
|
-
if (callbacks?.onDisconnect) this.onDisconnectCallbacks.delete(callbacks.onDisconnect);
|
|
769
|
-
if (callbacks?.onError) this.onErrorCallbacks.delete(callbacks.onError);
|
|
770
|
-
if (this.subscriptions.size === 0) {
|
|
771
|
-
this.log.debug("No more subscriptions, disconnecting");
|
|
772
|
-
this.disconnect();
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Connect to WebSocket server
|
|
778
|
-
*/
|
|
779
|
-
async connect() {
|
|
780
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
781
|
-
this.log.trace("Already connected, skipping connect");
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
this.isConnecting = true;
|
|
785
|
-
this.isError = false;
|
|
786
|
-
this.error = void 0;
|
|
787
|
-
const url = this.buildUrl();
|
|
788
|
-
this.log.info("Connecting to WebSocket server", { url });
|
|
789
|
-
return new Promise((resolve, reject) => {
|
|
790
|
-
try {
|
|
791
|
-
const ws$1 = new WebSocket(url);
|
|
792
|
-
this.ws = ws$1;
|
|
793
|
-
ws$1.onopen = () => {
|
|
794
|
-
this.isConnected = true;
|
|
795
|
-
this.isConnecting = false;
|
|
796
|
-
this.isError = false;
|
|
797
|
-
this.error = void 0;
|
|
798
|
-
this.reconnectAttempts = 0;
|
|
799
|
-
this.log.info("WebSocket connected", {
|
|
800
|
-
channelPath: this.channel.options.path,
|
|
801
|
-
rooms: Array.from(this.subscriptions.keys())
|
|
802
|
-
});
|
|
803
|
-
if (this.messageQueue.length > 0) this.log.debug("Flushing queued messages", { count: this.messageQueue.length });
|
|
804
|
-
while (this.messageQueue.length > 0) {
|
|
805
|
-
const msg = this.messageQueue.shift();
|
|
806
|
-
if (msg) {
|
|
807
|
-
this.log.trace("Sending queued message", { roomId: msg.roomId });
|
|
808
|
-
ws$1.send(JSON.stringify({
|
|
809
|
-
roomId: msg.roomId,
|
|
810
|
-
message: msg.message
|
|
811
|
-
}));
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
for (const callback of this.onConnectCallbacks) callback();
|
|
815
|
-
resolve();
|
|
816
|
-
};
|
|
817
|
-
ws$1.onmessage = (event) => {
|
|
818
|
-
this.log.trace("Message received", { dataLength: event.data?.length });
|
|
819
|
-
this.handleMessage(event.data);
|
|
820
|
-
};
|
|
821
|
-
ws$1.onclose = (event) => {
|
|
822
|
-
this.isConnected = false;
|
|
823
|
-
this.isConnecting = false;
|
|
824
|
-
this.ws = void 0;
|
|
825
|
-
this.log.info("WebSocket disconnected", {
|
|
826
|
-
code: event.code,
|
|
827
|
-
reason: event.reason,
|
|
828
|
-
wasClean: event.wasClean
|
|
829
|
-
});
|
|
830
|
-
for (const callback of this.onDisconnectCallbacks) callback();
|
|
831
|
-
if (this.options.autoReconnect !== false) this.scheduleReconnect();
|
|
832
|
-
};
|
|
833
|
-
ws$1.onerror = () => {
|
|
834
|
-
const err = /* @__PURE__ */ new Error("WebSocket connection error");
|
|
835
|
-
this.isError = true;
|
|
836
|
-
this.error = err;
|
|
837
|
-
this.isConnecting = false;
|
|
838
|
-
this.log.error("WebSocket error", { url });
|
|
839
|
-
for (const callback of this.onErrorCallbacks) callback(err);
|
|
840
|
-
reject(err);
|
|
841
|
-
};
|
|
842
|
-
} catch (err) {
|
|
843
|
-
const error = err instanceof Error ? err : /* @__PURE__ */ new Error("Connection failed");
|
|
844
|
-
this.isError = true;
|
|
845
|
-
this.error = error;
|
|
846
|
-
this.isConnecting = false;
|
|
847
|
-
this.log.error("Failed to create WebSocket", { error: error.message });
|
|
848
|
-
for (const callback of this.onErrorCallbacks) callback(error);
|
|
849
|
-
reject(error);
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Handle incoming message
|
|
855
|
-
*/
|
|
856
|
-
handleMessage(data) {
|
|
857
|
-
try {
|
|
858
|
-
const parsed = JSON.parse(data);
|
|
859
|
-
this.log.trace("Parsed incoming message", { parsed });
|
|
860
|
-
const inSchema = this.channel.options.schema.in;
|
|
861
|
-
this.alepha.codec.validate(inSchema, parsed);
|
|
862
|
-
this.log.debug("Dispatching message to handlers", { handlerCount: this.subscriptions.size });
|
|
863
|
-
for (const handler of this.subscriptions.values()) handler(parsed);
|
|
864
|
-
} catch (err) {
|
|
865
|
-
this.log.error("Error handling message:", err);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Send message to a specific room
|
|
870
|
-
*/
|
|
871
|
-
async send(roomId, message) {
|
|
872
|
-
this.log.trace("Sending message", {
|
|
873
|
-
roomId,
|
|
874
|
-
message
|
|
875
|
-
});
|
|
876
|
-
const outSchema = this.channel.options.schema.out;
|
|
877
|
-
if (!alepha.TypeBoxValue.Check(outSchema, message)) {
|
|
878
|
-
const errors = Array.from(alepha.TypeBoxValue.Errors(outSchema, message));
|
|
879
|
-
this.log.warn("Message validation failed", { errors });
|
|
880
|
-
throw new Error(`Message validation failed: ${errors.map((e) => e.message).join(", ")}`);
|
|
881
|
-
}
|
|
882
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
883
|
-
this.log.debug("Connection not ready, queuing message", {
|
|
884
|
-
roomId,
|
|
885
|
-
queueSize: this.messageQueue.length + 1
|
|
886
|
-
});
|
|
887
|
-
this.messageQueue.push({
|
|
888
|
-
roomId,
|
|
889
|
-
message
|
|
890
|
-
});
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
this.log.debug("Sending message to server", { roomId });
|
|
894
|
-
this.ws.send(JSON.stringify({
|
|
895
|
-
roomId,
|
|
896
|
-
message
|
|
897
|
-
}));
|
|
898
|
-
}
|
|
899
|
-
/**
|
|
900
|
-
* Schedule reconnection
|
|
901
|
-
*/
|
|
902
|
-
scheduleReconnect() {
|
|
903
|
-
const maxAttempts = this.options.maxReconnectAttempts ?? this.env.WEBSOCKET_MAX_RECONNECT_ATTEMPTS ?? 10;
|
|
904
|
-
const reconnectInterval = this.options.reconnectInterval ?? this.env.WEBSOCKET_RECONNECT_INTERVAL ?? 3e3;
|
|
905
|
-
if (maxAttempts !== -1 && this.reconnectAttempts >= maxAttempts) {
|
|
906
|
-
this.log.warn("Max reconnection attempts reached", {
|
|
907
|
-
attempts: this.reconnectAttempts,
|
|
908
|
-
maxAttempts
|
|
909
|
-
});
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
this.reconnectAttempts++;
|
|
913
|
-
this.log.debug("Scheduling reconnection", {
|
|
914
|
-
attempt: this.reconnectAttempts,
|
|
915
|
-
maxAttempts,
|
|
916
|
-
intervalMs: reconnectInterval
|
|
917
|
-
});
|
|
918
|
-
this.reconnectTimer = window.setTimeout(() => {
|
|
919
|
-
this.log.info("Reconnecting...", {
|
|
920
|
-
attempt: this.reconnectAttempts,
|
|
921
|
-
maxAttempts
|
|
922
|
-
});
|
|
923
|
-
this.connect().catch((error) => {
|
|
924
|
-
this.log.error("Reconnection failed:", error);
|
|
925
|
-
});
|
|
926
|
-
}, reconnectInterval);
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Disconnect from server
|
|
930
|
-
*/
|
|
931
|
-
disconnect() {
|
|
932
|
-
this.log.debug("Disconnecting", {
|
|
933
|
-
hasTimer: !!this.reconnectTimer,
|
|
934
|
-
hasConnection: !!this.ws
|
|
935
|
-
});
|
|
936
|
-
if (this.reconnectTimer) {
|
|
937
|
-
clearTimeout(this.reconnectTimer);
|
|
938
|
-
this.reconnectTimer = void 0;
|
|
939
|
-
}
|
|
940
|
-
if (this.ws) {
|
|
941
|
-
this.ws.close();
|
|
942
|
-
this.ws = void 0;
|
|
943
|
-
}
|
|
944
|
-
this.isConnected = false;
|
|
945
|
-
this.isConnecting = false;
|
|
946
|
-
this.log.info("Disconnected");
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Reconnect manually
|
|
950
|
-
*/
|
|
951
|
-
reconnect() {
|
|
952
|
-
this.log.info("Manual reconnect requested");
|
|
953
|
-
this.disconnect();
|
|
954
|
-
this.connect().catch((error) => {
|
|
955
|
-
this.log.error("Manual reconnection failed:", error);
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Check if subscribed to a room
|
|
960
|
-
*/
|
|
961
|
-
hasRoom(roomId) {
|
|
962
|
-
return this.subscriptions.has(roomId);
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* Get all subscribed rooms
|
|
966
|
-
*/
|
|
967
|
-
getRooms() {
|
|
968
|
-
return Array.from(this.subscriptions.keys());
|
|
969
|
-
}
|
|
970
|
-
};
|
|
971
|
-
/**
|
|
972
|
-
* WebSocket Client Service
|
|
973
|
-
*
|
|
974
|
-
* Manages WebSocket connections from the client side (browser).
|
|
975
|
-
* One connection per channel, multiple rooms per connection.
|
|
976
|
-
*/
|
|
977
|
-
var WebSocketClient = class {
|
|
978
|
-
log = (0, alepha_logger.$logger)();
|
|
979
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
980
|
-
env = (0, alepha.$env)(envSchema);
|
|
981
|
-
connections = /* @__PURE__ */ new Map();
|
|
982
|
-
/**
|
|
983
|
-
* Subscribe to a room on a channel
|
|
984
|
-
*/
|
|
985
|
-
subscribe(roomId, channel, handler, options = {}) {
|
|
986
|
-
const channelPath = channel.options.path;
|
|
987
|
-
this.log.debug("WebSocketClient.subscribe", {
|
|
988
|
-
roomId,
|
|
989
|
-
channelPath,
|
|
990
|
-
existingConnections: this.connections.size
|
|
991
|
-
});
|
|
992
|
-
let connection = this.connections.get(channelPath);
|
|
993
|
-
if (!connection) {
|
|
994
|
-
this.log.debug("Creating new connection for channel", { channelPath });
|
|
995
|
-
connection = this.alepha.inject(WebSocketChannelConnection, {
|
|
996
|
-
lifetime: "transient",
|
|
997
|
-
args: [
|
|
998
|
-
channel,
|
|
999
|
-
{
|
|
1000
|
-
url: options.url,
|
|
1001
|
-
autoReconnect: options.autoReconnect,
|
|
1002
|
-
reconnectInterval: options.reconnectInterval,
|
|
1003
|
-
maxReconnectAttempts: options.maxReconnectAttempts
|
|
1004
|
-
},
|
|
1005
|
-
this.env
|
|
1006
|
-
]
|
|
1007
|
-
});
|
|
1008
|
-
this.connections.set(channelPath, connection);
|
|
1009
|
-
} else this.log.trace("Reusing existing connection for channel", { channelPath });
|
|
1010
|
-
const unsubscribe = connection.subscribe(roomId, handler, {
|
|
1011
|
-
onConnect: options.onConnect,
|
|
1012
|
-
onDisconnect: options.onDisconnect,
|
|
1013
|
-
onError: options.onError
|
|
1014
|
-
});
|
|
1015
|
-
return () => {
|
|
1016
|
-
this.log.debug("WebSocketClient.unsubscribe", {
|
|
1017
|
-
roomId,
|
|
1018
|
-
channelPath
|
|
1019
|
-
});
|
|
1020
|
-
unsubscribe();
|
|
1021
|
-
if (connection.getRooms().length === 0) {
|
|
1022
|
-
this.log.debug("Removing connection for channel (no more rooms)", { channelPath });
|
|
1023
|
-
this.connections.delete(channelPath);
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Send message to a room on a channel
|
|
1029
|
-
*/
|
|
1030
|
-
async send(roomId, channel, message) {
|
|
1031
|
-
const channelPath = channel.options.path;
|
|
1032
|
-
this.log.trace("WebSocketClient.send", {
|
|
1033
|
-
roomId,
|
|
1034
|
-
channelPath
|
|
1035
|
-
});
|
|
1036
|
-
const connection = this.connections.get(channelPath);
|
|
1037
|
-
if (!connection) {
|
|
1038
|
-
this.log.warn("Attempted to send on unsubscribed channel", { channelPath });
|
|
1039
|
-
throw new alepha.AlephaError(`Not subscribed to channel ${channelPath}. Subscribe first before sending messages.`);
|
|
1040
|
-
}
|
|
1041
|
-
await connection.send(roomId, message);
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* Get connection for a channel
|
|
1045
|
-
*/
|
|
1046
|
-
getConnection(channel) {
|
|
1047
|
-
const channelPath = channel.options.path;
|
|
1048
|
-
const connection = this.connections.get(channelPath);
|
|
1049
|
-
this.log.trace("WebSocketClient.getConnection", {
|
|
1050
|
-
channelPath,
|
|
1051
|
-
found: !!connection
|
|
1052
|
-
});
|
|
1053
|
-
return connection;
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Disconnect all connections
|
|
1057
|
-
*/
|
|
1058
|
-
disconnectAll() {
|
|
1059
|
-
this.log.info("Disconnecting all connections", { count: this.connections.size });
|
|
1060
|
-
for (const connection of this.connections.values()) connection.disconnect();
|
|
1061
|
-
this.connections.clear();
|
|
1062
|
-
this.log.debug("All connections disconnected");
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
//#endregion
|
|
1067
|
-
//#region src/websocket/index.ts
|
|
1068
|
-
/**
|
|
1069
|
-
* Provides real-time bidirectional communication using WebSockets.
|
|
1070
|
-
*
|
|
1071
|
-
* The WebSockets module enables building real-time applications using the `$websocket` descriptor
|
|
1072
|
-
* on class properties. It provides automatic connection management, message routing, type-safe
|
|
1073
|
-
* message handling, and seamless integration with other Alepha modules.
|
|
1074
|
-
*
|
|
1075
|
-
* On the server side (Node.js), it uses the 'ws' library to create a WebSocket server.
|
|
1076
|
-
* On the client side (browser), it uses the native WebSocket API.
|
|
1077
|
-
*
|
|
1078
|
-
* @see {@link $websocket}
|
|
1079
|
-
* @module alepha.websockets
|
|
1080
|
-
*/
|
|
1081
|
-
const AlephaWebSocket = (0, alepha.$module)({
|
|
1082
|
-
name: "alepha.websocket",
|
|
1083
|
-
descriptors: [$channel, $websocket],
|
|
1084
|
-
services: [
|
|
1085
|
-
WebSocketServerProvider,
|
|
1086
|
-
NodeWebSocketServerProvider,
|
|
1087
|
-
RoomManager,
|
|
1088
|
-
WebSocketTopicService
|
|
1089
|
-
],
|
|
1090
|
-
register: (alepha$1) => {
|
|
1091
|
-
alepha$1.with(alepha_server.AlephaServer);
|
|
1092
|
-
alepha$1.with(alepha_topic.AlephaTopic);
|
|
1093
|
-
alepha$1.with({
|
|
1094
|
-
provide: WebSocketServerProvider,
|
|
1095
|
-
use: NodeWebSocketServerProvider
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
//#endregion
|
|
1101
|
-
exports.$channel = $channel;
|
|
1102
|
-
exports.$websocket = $websocket;
|
|
1103
|
-
exports.AlephaWebSocket = AlephaWebSocket;
|
|
1104
|
-
exports.ChannelDescriptor = ChannelDescriptor;
|
|
1105
|
-
exports.NodeWebSocketConnection = NodeWebSocketConnection;
|
|
1106
|
-
exports.NodeWebSocketServerProvider = NodeWebSocketServerProvider;
|
|
1107
|
-
exports.RoomManager = RoomManager;
|
|
1108
|
-
exports.WebSocketChannelConnection = WebSocketChannelConnection;
|
|
1109
|
-
exports.WebSocketClient = WebSocketClient;
|
|
1110
|
-
exports.WebSocketConnectionError = WebSocketConnectionError;
|
|
1111
|
-
exports.WebSocketDescriptor = WebSocketDescriptor;
|
|
1112
|
-
exports.WebSocketError = WebSocketError;
|
|
1113
|
-
exports.WebSocketServerProvider = WebSocketServerProvider;
|
|
1114
|
-
exports.WebSocketState = WebSocketState;
|
|
1115
|
-
exports.WebSocketTopicService = WebSocketTopicService;
|
|
1116
|
-
exports.WebSocketValidationError = WebSocketValidationError;
|
|
1117
|
-
//# sourceMappingURL=index.cjs.map
|