alepha 0.20.6 → 0.20.7

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