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,617 +0,0 @@
1
- import type { IncomingMessage } from "node:http";
2
- import {
3
- $atom,
4
- $hook,
5
- $inject,
6
- $state,
7
- Alepha,
8
- AlephaError,
9
- SchemaValidator,
10
- type Static,
11
- t,
12
- } from "alepha";
13
- import { $logger } from "alepha/logger";
14
- import { WebSocket, WebSocketServer } from "ws";
15
- import { WebSocketValidationError } from "../errors/WebSocketError.ts";
16
- import type {
17
- EmitOptions,
18
- WebSocketConnection,
19
- WebSocketHandlerContext,
20
- WebSocketPrimitiveOptions,
21
- WebSocketState,
22
- } from "../interfaces/WebSocketInterfaces.ts";
23
- import type { TWSObject } from "../primitives/$channel.ts";
24
- import { RoomManager } from "../services/RoomManager.ts";
25
- import { WebSocketTopicService } from "../services/WebSocketTopicService.ts";
26
- import { WebSocketServerProvider } from "./WebSocketServerProvider.ts";
27
-
28
- // ---------------------------------------------------------------------------------------------------------------------
29
-
30
- /**
31
- * WebSocket configuration atom.
32
- */
33
- export const websocketOptions = $atom({
34
- name: "alepha.websocket.options",
35
- schema: t.object({
36
- path: t.text({
37
- default: "/ws",
38
- description: "Base path for WebSocket endpoints.",
39
- }),
40
- }),
41
- default: {
42
- path: "/ws",
43
- },
44
- });
45
-
46
- export type WebSocketOptions = Static<typeof websocketOptions.schema>;
47
-
48
- declare module "alepha" {
49
- interface State {
50
- [websocketOptions.key]: WebSocketOptions;
51
- }
52
- }
53
-
54
- // ---------------------------------------------------------------------------------------------------------------------
55
-
56
- export class NodeWebSocketServerProvider extends WebSocketServerProvider {
57
- protected readonly alepha = $inject(Alepha);
58
- protected readonly roomManager = $inject(RoomManager);
59
- protected readonly topicService = $inject(WebSocketTopicService);
60
- protected readonly log = $logger();
61
- protected readonly wsOptions = $state(websocketOptions);
62
-
63
- protected wss?: WebSocketServer;
64
- protected endpoints = new Map<string, WebSocketPrimitiveOptions<any, any>>();
65
- protected connections = new Map<string, WebSocketConnection>();
66
- protected userConnections = new Map<string, Set<string>>(); // userId → Set<connectionId>
67
- protected nextConnectionId = 1;
68
-
69
- // -------------------------------------------------------------------------------------------------------------------
70
-
71
- public registerEndpoint<TClient extends TWSObject, TServer extends TWSObject>(
72
- config: WebSocketPrimitiveOptions<TClient, TServer>,
73
- ): void {
74
- const path = config.channel.options.path;
75
- this.endpoints.set(path, config);
76
- }
77
-
78
- public async emit<TClient extends TWSObject>(
79
- channelPath: string,
80
- options: EmitOptions<TClient>,
81
- ): Promise<void> {
82
- // Publish to topic so all server instances receive it
83
- await this.topicService.publish({
84
- channelPath,
85
- roomIds: options.roomIds
86
- ? options.roomIds
87
- : options.roomId
88
- ? [options.roomId]
89
- : undefined,
90
- userIds: options.userIds
91
- ? options.userIds
92
- : options.userId
93
- ? [options.userId]
94
- : undefined,
95
- connectionIds: options.connectionIds
96
- ? options.connectionIds
97
- : options.connectionId
98
- ? [options.connectionId]
99
- : undefined,
100
- exceptConnectionIds: options.exceptConnectionIds,
101
- exceptUserIds: options.exceptUserIds,
102
- message: options.message,
103
- });
104
- }
105
-
106
- public getConnections(): WebSocketConnection[] {
107
- return Array.from(this.connections.values());
108
- }
109
-
110
- public getRoomConnections(roomId: string): WebSocketConnection[] {
111
- const connectionIds = this.roomManager.getRoomConnections(roomId);
112
- return connectionIds
113
- .map((id) => this.connections.get(id))
114
- .filter((conn): conn is WebSocketConnection => conn !== undefined);
115
- }
116
-
117
- public getUserConnections(userId: string): WebSocketConnection[] {
118
- const connectionIds = this.userConnections.get(userId);
119
- if (!connectionIds) {
120
- return [];
121
- }
122
- return Array.from(connectionIds)
123
- .map((id) => this.connections.get(id))
124
- .filter((conn): conn is WebSocketConnection => conn !== undefined);
125
- }
126
-
127
- public async closeConnection(
128
- connectionId: string,
129
- code?: number,
130
- reason?: string,
131
- ): Promise<void> {
132
- const connection = this.connections.get(connectionId);
133
- if (!connection) {
134
- this.log.warn(`Connection not found: ${connectionId}`);
135
- return;
136
- }
137
- await connection.close(code, reason);
138
- }
139
-
140
- // -------------------------------------------------------------------------------------------------------------------
141
-
142
- protected handleUpgrade(
143
- request: IncomingMessage,
144
- socket: any,
145
- head: Buffer,
146
- ): boolean {
147
- const url = new URL(request.url || "/", "http://localhost");
148
- const path = url.pathname;
149
-
150
- const endpoint = this.endpoints.get(path);
151
- if (!endpoint) {
152
- // Not our endpoint - in Vite dev mode, let Vite HMR handle it
153
- // In production, destroy the socket
154
- if (!this.alepha.isViteDev()) {
155
- this.log.warn(`No WebSocket endpoint found for path: ${path}`);
156
- socket.destroy();
157
- }
158
- return false;
159
- }
160
-
161
- this.log.debug(`WebSocket upgrade request: ${path}`);
162
-
163
- this.wss?.handleUpgrade(request, socket, head, (ws) => {
164
- this.handleConnection(ws, endpoint, request);
165
- });
166
-
167
- return true;
168
- }
169
-
170
- protected handleConnection<
171
- TClient extends TWSObject,
172
- TServer extends TWSObject,
173
- >(
174
- ws: WebSocket,
175
- endpoint: WebSocketPrimitiveOptions<TClient, TServer>,
176
- request: IncomingMessage,
177
- ): void {
178
- const connectionId = `ws-${this.nextConnectionId++}`;
179
-
180
- // TODO: Extract userId from the WebSocket upgrade request.
181
- // Parse JWT from cookies or Authorization header during handshake.
182
- // Until implemented, maxConnectionsPerUser has no effect.
183
- const userId: string | undefined = undefined;
184
-
185
- // Extract roomIds from query params (e.g., ?roomId=room1&roomId=room2 or ?roomIds=room1,room2)
186
- const url = new URL(request.url || "/", "http://localhost");
187
- const roomIds = this.extractRoomIds(url);
188
-
189
- // Check max connections per user before registering
190
- if (userId && endpoint.maxConnectionsPerUser) {
191
- const existingConns = this.userConnections.get(userId);
192
- if (
193
- existingConns &&
194
- existingConns.size >= endpoint.maxConnectionsPerUser
195
- ) {
196
- this.log.warn(
197
- `User ${userId} exceeded max connections (${endpoint.maxConnectionsPerUser})`,
198
- );
199
- ws.close(1008, "Max connections per user exceeded");
200
- return;
201
- }
202
- }
203
-
204
- const connection = this.alepha.inject(NodeWebSocketConnection, {
205
- lifetime: "transient",
206
- args: [connectionId, userId, roomIds, ws, this, endpoint],
207
- });
208
-
209
- this.connections.set(connectionId, connection);
210
-
211
- // Track user connections
212
- if (userId) {
213
- let userConns = this.userConnections.get(userId);
214
- if (!userConns) {
215
- userConns = new Set();
216
- this.userConnections.set(userId, userConns);
217
- }
218
- userConns.add(connectionId);
219
- }
220
-
221
- // Join rooms
222
- if (roomIds.length > 0) {
223
- this.roomManager.joinRooms(connectionId, roomIds);
224
- }
225
-
226
- this.log.info(`WebSocket connection established: ${connectionId}`, {
227
- path: endpoint.channel.options.path,
228
- userId,
229
- roomIds,
230
- remoteAddress: request.socket.remoteAddress,
231
- });
232
-
233
- // Call onConnect handler if provided
234
- if (endpoint.onConnect) {
235
- Promise.resolve(
236
- endpoint.onConnect({ connectionId, userId, roomIds }),
237
- ).catch((error) => {
238
- this.log.error("Error in onConnect handler:", error);
239
- });
240
- }
241
-
242
- ws.on("message", (data) => {
243
- connection.handleMessage(data).catch((error) => {
244
- this.log.error(
245
- `Unhandled error in message handler for ${connectionId}:`,
246
- error,
247
- );
248
- });
249
- });
250
-
251
- ws.on("close", (code, reason) => {
252
- this.log.info(`WebSocket connection closed: ${connectionId}`, {
253
- code,
254
- reason: reason.toString(),
255
- });
256
-
257
- // Clean up
258
- this.connections.delete(connectionId);
259
- this.roomManager.leaveAllRooms(connectionId);
260
-
261
- if (userId) {
262
- const userConns = this.userConnections.get(userId);
263
- if (userConns) {
264
- userConns.delete(connectionId);
265
- if (userConns.size === 0) {
266
- this.userConnections.delete(userId);
267
- }
268
- }
269
- }
270
-
271
- // Call onDisconnect handler if provided
272
- if (endpoint.onDisconnect) {
273
- Promise.resolve(
274
- endpoint.onDisconnect({ connectionId, userId, roomIds }),
275
- ).catch((error) => {
276
- this.log.error("Error in onDisconnect handler:", error);
277
- });
278
- }
279
- });
280
-
281
- ws.on("error", (error) => {
282
- this.log.error(`WebSocket error on ${connectionId}:`, error);
283
- });
284
- }
285
-
286
- protected extractRoomIds(url: URL): string[] {
287
- const roomIds: string[] = [];
288
-
289
- // Check for roomId parameter (can be multiple)
290
- const roomIdParams = url.searchParams.getAll("roomId");
291
- roomIds.push(...roomIdParams);
292
-
293
- // Check for roomIds parameter (comma-separated)
294
- const roomIdsParam = url.searchParams.get("roomIds");
295
- if (roomIdsParam) {
296
- roomIds.push(
297
- ...roomIdsParam
298
- .split(",")
299
- .map((id) => id.trim())
300
- .filter((id) => id.length > 0),
301
- );
302
- }
303
-
304
- // Default room if none specified
305
- if (roomIds.length === 0) {
306
- roomIds.push("default");
307
- }
308
-
309
- return roomIds;
310
- }
311
-
312
- /**
313
- * Send message to local connections based on targeting criteria
314
- * This is called by the topic service when a message is received
315
- */
316
- protected async sendToLocalConnections(
317
- channelPath: string,
318
- message: any,
319
- criteria: {
320
- roomIds?: string[];
321
- userIds?: string[];
322
- connectionIds?: string[];
323
- exceptConnectionIds?: string[];
324
- exceptUserIds?: string[];
325
- },
326
- ): Promise<void> {
327
- const targetConnections = new Set<string>();
328
-
329
- // Helper to check if a connection belongs to this channel
330
- const isOnChannel = (connId: string) => {
331
- const conn = this.connections.get(connId);
332
- return conn?.channelPath === channelPath;
333
- };
334
-
335
- // Collect target connections based on criteria
336
- if (criteria.roomIds) {
337
- for (const roomId of criteria.roomIds) {
338
- const roomConns = this.roomManager.getRoomConnections(roomId);
339
- for (const connId of roomConns) {
340
- if (isOnChannel(connId)) {
341
- targetConnections.add(connId);
342
- }
343
- }
344
- }
345
- }
346
-
347
- if (criteria.userIds) {
348
- for (const userId of criteria.userIds) {
349
- const userConns = this.userConnections.get(userId);
350
- if (userConns) {
351
- for (const connId of userConns) {
352
- if (isOnChannel(connId)) {
353
- targetConnections.add(connId);
354
- }
355
- }
356
- }
357
- }
358
- }
359
-
360
- if (criteria.connectionIds) {
361
- for (const connId of criteria.connectionIds) {
362
- if (isOnChannel(connId)) {
363
- targetConnections.add(connId);
364
- }
365
- }
366
- }
367
-
368
- // If no specific targeting, send to all connections on this channel
369
- if (!criteria.roomIds && !criteria.userIds && !criteria.connectionIds) {
370
- for (const conn of this.connections.values()) {
371
- if (conn.channelPath === channelPath) {
372
- targetConnections.add(conn.id);
373
- }
374
- }
375
- }
376
-
377
- // Remove exceptions
378
- if (criteria.exceptConnectionIds) {
379
- for (const connId of criteria.exceptConnectionIds) {
380
- targetConnections.delete(connId);
381
- }
382
- }
383
-
384
- if (criteria.exceptUserIds) {
385
- for (const userId of criteria.exceptUserIds) {
386
- const userConns = this.userConnections.get(userId);
387
- if (userConns) {
388
- for (const connId of userConns) {
389
- targetConnections.delete(connId);
390
- }
391
- }
392
- }
393
- }
394
-
395
- // Send to all target connections
396
- const serialized = JSON.stringify(message);
397
- await Promise.all(
398
- Array.from(targetConnections).map(async (connId) => {
399
- const conn = this.connections.get(connId);
400
- if (conn) {
401
- try {
402
- await conn.send(serialized);
403
- } catch (error) {
404
- this.log.error(`Failed to send to connection ${connId}:`, error);
405
- }
406
- }
407
- }),
408
- );
409
- }
410
-
411
- // -------------------------------------------------------------------------------------------------------------------
412
-
413
- protected readonly start = $hook({
414
- on: "start",
415
- handler: async () => {
416
- if (this.alepha.isServerless()) {
417
- this.log.debug("WebSocket server disabled in serverless mode");
418
- return;
419
- }
420
-
421
- this.wss = new WebSocketServer({ noServer: true });
422
-
423
- for (const [path, endpoint] of this.endpoints.entries()) {
424
- this.log.debug(`WebSocket endpoint registered: ${path}`);
425
- }
426
-
427
- // Set up topic service message handler
428
- this.topicService.setMessageHandler(async (event) => {
429
- await this.sendToLocalConnections(event.channelPath, event.message, {
430
- roomIds: event.roomIds,
431
- userIds: event.userIds,
432
- connectionIds: event.connectionIds,
433
- exceptConnectionIds: event.exceptConnectionIds,
434
- exceptUserIds: event.exceptUserIds,
435
- });
436
- });
437
-
438
- this.log.info("WebSocket server OK", {
439
- basePath: this.wsOptions.path,
440
- });
441
- },
442
- });
443
-
444
- protected readonly ready = $hook({
445
- on: "ready",
446
- handler: async () => {
447
- if (this.alepha.isServerless() || !this.wss) {
448
- return;
449
- }
450
-
451
- // Attach upgrade handler to the HTTP server (must be done after HTTP server starts)
452
- const httpServer = this.alepha.store.get("alepha.node.server");
453
- if (httpServer) {
454
- httpServer.on("upgrade", (request, socket, head) => {
455
- this.handleUpgrade(request, socket, head);
456
- });
457
- this.log.debug("WebSocket upgrade handler attached to HTTP server");
458
- } else {
459
- this.log.warn(
460
- "No HTTP server found - WebSocket upgrade handler not attached",
461
- );
462
- }
463
- },
464
- });
465
-
466
- protected readonly stop = $hook({
467
- on: "stop",
468
- handler: async () => {
469
- if (!this.wss) {
470
- return;
471
- }
472
-
473
- // Close all connections (collect into array to avoid mutation during iteration)
474
- const connections = Array.from(this.connections.values());
475
- for (const connection of connections) {
476
- await connection.close(1001, "Server shutting down");
477
- }
478
-
479
- await new Promise<void>((resolve, reject) => {
480
- this.wss?.close((err) => {
481
- if (err) {
482
- reject(err);
483
- } else {
484
- resolve();
485
- }
486
- });
487
- });
488
-
489
- this.log.info("WebSocket server closed");
490
- },
491
- });
492
- }
493
-
494
- // ---------------------------------------------------------------------------------------------------------------------
495
-
496
- export class NodeWebSocketConnection implements WebSocketConnection {
497
- protected readonly log = $logger();
498
- protected readonly schemaValidator = $inject(SchemaValidator);
499
- public metadata?: Record<string, any>;
500
-
501
- constructor(
502
- public readonly id: string,
503
- public readonly userId: string | undefined,
504
- public readonly roomIds: string[],
505
- protected readonly ws: WebSocket,
506
- protected readonly provider: NodeWebSocketServerProvider,
507
- protected readonly endpoint: WebSocketPrimitiveOptions<any, any>,
508
- ) {}
509
-
510
- public get channelPath(): string {
511
- return this.endpoint.channel.options.path;
512
- }
513
-
514
- public get readyState(): WebSocketState {
515
- return this.ws.readyState;
516
- }
517
-
518
- public async send(message: any): Promise<void> {
519
- if (this.ws.readyState !== WebSocket.OPEN) {
520
- throw new AlephaError("WebSocket is not open");
521
- }
522
-
523
- const data =
524
- typeof message === "string" ? message : JSON.stringify(message);
525
- await new Promise<void>((resolve, reject) => {
526
- this.ws.send(data, (err) => {
527
- if (err) {
528
- reject(err);
529
- } else {
530
- resolve();
531
- }
532
- });
533
- });
534
- }
535
-
536
- public async close(code?: number, reason?: string): Promise<void> {
537
- this.ws.close(code, reason);
538
- }
539
-
540
- public async handleMessage(data: any): Promise<void> {
541
- try {
542
- const rawMessage = data.toString();
543
- let parsed: any;
544
-
545
- try {
546
- parsed = JSON.parse(rawMessage);
547
- } catch {
548
- this.log.warn("Received non-JSON message");
549
- return;
550
- }
551
-
552
- // Extract roomId from message (or use first room in connection's rooms)
553
- const roomId = parsed.roomId || this.roomIds[0] || "default";
554
-
555
- // Extract message payload
556
- const message = parsed.message || parsed;
557
-
558
- // Validate message against schema (out = client→server)
559
- const outSchema = this.endpoint.channel.options.schema.out;
560
- try {
561
- this.schemaValidator.validate(outSchema, message, {
562
- trim: false,
563
- nullToUndefined: false,
564
- deleteUndefined: false,
565
- });
566
- } catch (err) {
567
- throw new WebSocketValidationError(
568
- `Message validation failed: ${(err as Error).message}`,
569
- );
570
- }
571
-
572
- // Create reply function scoped to this context
573
- const reply = async (options: {
574
- message: any;
575
- roomId?: string;
576
- exceptSelf?: boolean;
577
- exceptConnectionIds?: string[];
578
- exceptUserIds?: string[];
579
- }) => {
580
- const targetRoomId = options.roomId || roomId;
581
- const exceptConnectionIds = options.exceptConnectionIds || [];
582
-
583
- if (options.exceptSelf) {
584
- exceptConnectionIds.push(this.id);
585
- }
586
-
587
- await this.provider.emit(this.endpoint.channel.options.path, {
588
- message: options.message,
589
- roomId: targetRoomId,
590
- exceptConnectionIds,
591
- exceptUserIds: options.exceptUserIds,
592
- });
593
- };
594
-
595
- const context: WebSocketHandlerContext<any, any> = {
596
- connectionId: this.id,
597
- userId: this.userId,
598
- roomId,
599
- message,
600
- reply,
601
- };
602
-
603
- await this.endpoint.handler(context);
604
- } catch (error) {
605
- this.log.error(`Error handling WebSocket message on ${this.id}:`, error);
606
-
607
- // Send error back to client (best-effort, may not match channel schema)
608
- try {
609
- await this.send({
610
- error: error instanceof Error ? error.message : "Unknown error",
611
- });
612
- } catch {
613
- // Connection may already be closed
614
- }
615
- }
616
- }
617
- }
@@ -1,56 +0,0 @@
1
- import type {
2
- EmitOptions,
3
- WebSocketConnection,
4
- WebSocketPrimitiveOptions,
5
- } from "../interfaces/WebSocketInterfaces.ts";
6
- import type { TWSObject } from "../primitives/$channel.ts";
7
-
8
- /**
9
- * Abstract WebSocket server provider
10
- *
11
- * This class provides the base interface that must be implemented by
12
- * platform-specific providers (Node.js, Browser, etc.)
13
- */
14
- export abstract class WebSocketServerProvider {
15
- /**
16
- * Register a WebSocket endpoint with its channel configuration
17
- */
18
- abstract registerEndpoint<
19
- TClient extends TWSObject,
20
- TServer extends TWSObject,
21
- >(config: WebSocketPrimitiveOptions<TClient, TServer>): void;
22
-
23
- /**
24
- * Emit a message to clients based on targeting criteria
25
- *
26
- * This method distributes messages across all server instances via pub/sub.
27
- */
28
- abstract emit<TClient extends TWSObject>(
29
- channelPath: string,
30
- options: EmitOptions<TClient>,
31
- ): Promise<void>;
32
-
33
- /**
34
- * Get all active connections (local to this server instance)
35
- */
36
- abstract getConnections(): WebSocketConnection[];
37
-
38
- /**
39
- * Get connections in a specific room (local to this server instance)
40
- */
41
- abstract getRoomConnections(roomId: string): WebSocketConnection[];
42
-
43
- /**
44
- * Get connections for a specific user (local to this server instance)
45
- */
46
- abstract getUserConnections(userId: string): WebSocketConnection[];
47
-
48
- /**
49
- * Close a specific connection
50
- */
51
- abstract closeConnection(
52
- connectionId: string,
53
- code?: number,
54
- reason?: string,
55
- ): Promise<void>;
56
- }