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,951 +0,0 @@
1
- import { Alepha, t } from "alepha";
2
- import { NodeHttpServerProvider } from "alepha/server";
3
- import { describe, test } from "vitest";
4
- import WebSocket from "ws";
5
- import { AlephaWebSocket } from "../index.ts";
6
- import { $channel } from "../primitives/$channel.ts";
7
- import { $websocket } from "../primitives/$websocket.ts";
8
- import { NodeWebSocketServerProvider } from "../providers/NodeWebSocketServerProvider.ts";
9
- import { RoomManager } from "../services/RoomManager.ts";
10
-
11
- // Helpers
12
-
13
- function waitForOpen(ws: WebSocket) {
14
- return new Promise<void>((resolve) => ws.on("open", resolve));
15
- }
16
-
17
- function collectMessages(ws: WebSocket, messages: any[] = []) {
18
- ws.on("message", (data) => messages.push(JSON.parse(data.toString())));
19
- return messages;
20
- }
21
-
22
- function waitForMessage(ws: WebSocket): Promise<any> {
23
- return new Promise((resolve) =>
24
- ws.once("message", (data) => resolve(JSON.parse(data.toString()))),
25
- );
26
- }
27
-
28
- function waitForClose(
29
- ws: WebSocket,
30
- ): Promise<{ code: number; reason: string }> {
31
- return new Promise((resolve) =>
32
- ws.on("close", (code, reason) =>
33
- resolve({ code, reason: reason.toString() }),
34
- ),
35
- );
36
- }
37
-
38
- const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
39
-
40
- // Schemas used across tests
41
- const chatInSchema = t.object({
42
- type: t.text(),
43
- content: t.text(),
44
- });
45
-
46
- const chatOutSchema = t.object({
47
- content: t.text(),
48
- });
49
-
50
- describe("WebSocket integration", () => {
51
- // -------------------------------------------------------------------------------------------------------------------
52
- // Connection lifecycle
53
- // -------------------------------------------------------------------------------------------------------------------
54
-
55
- describe("connection lifecycle", () => {
56
- test("onConnect and onDisconnect callbacks", async ({ expect }) => {
57
- const events: string[] = [];
58
-
59
- const alepha = Alepha.create().with(AlephaWebSocket);
60
-
61
- class Controller {
62
- ch = $channel({
63
- path: "/ws/lifecycle",
64
- schema: { in: chatInSchema, out: chatOutSchema },
65
- });
66
-
67
- ws = $websocket({
68
- channel: this.ch,
69
- handler: async () => {},
70
- onConnect: ({ connectionId, roomIds }) => {
71
- events.push(`connect:${connectionId}:${roomIds.join(",")}`);
72
- },
73
- onDisconnect: ({ connectionId }) => {
74
- events.push(`disconnect:${connectionId}`);
75
- },
76
- });
77
- }
78
-
79
- alepha.inject(Controller);
80
- await alepha.start();
81
-
82
- const hostname = alepha
83
- .inject(NodeHttpServerProvider)
84
- .hostname.replace("http://", "ws://");
85
-
86
- const ws = new WebSocket(`${hostname}/ws/lifecycle?roomId=lobby`);
87
- await waitForOpen(ws);
88
- await delay(100);
89
-
90
- expect(events.length).toBe(1);
91
- expect(events[0]).toMatch(/^connect:ws-\d+:lobby$/);
92
-
93
- ws.close();
94
- await delay(100);
95
-
96
- expect(events.length).toBe(2);
97
- expect(events[1]).toMatch(/^disconnect:ws-\d+$/);
98
-
99
- await alepha.stop();
100
- });
101
-
102
- test("default room assignment when no roomId provided", async ({
103
- expect,
104
- }) => {
105
- const roomIds: string[] = [];
106
-
107
- const alepha = Alepha.create().with(AlephaWebSocket);
108
-
109
- class Controller {
110
- ch = $channel({
111
- path: "/ws/default-room",
112
- schema: { in: chatInSchema, out: chatOutSchema },
113
- });
114
-
115
- ws = $websocket({
116
- channel: this.ch,
117
- handler: async () => {},
118
- onConnect: ({ roomIds: ids }) => {
119
- roomIds.push(...ids);
120
- },
121
- });
122
- }
123
-
124
- alepha.inject(Controller);
125
- await alepha.start();
126
-
127
- const hostname = alepha
128
- .inject(NodeHttpServerProvider)
129
- .hostname.replace("http://", "ws://");
130
-
131
- const ws = new WebSocket(`${hostname}/ws/default-room`);
132
- await waitForOpen(ws);
133
- await delay(100);
134
-
135
- expect(roomIds).toEqual(["default"]);
136
-
137
- ws.close();
138
- await alepha.stop();
139
- });
140
-
141
- test("multiple roomIds via comma-separated query param", async ({
142
- expect,
143
- }) => {
144
- const roomIds: string[] = [];
145
-
146
- const alepha = Alepha.create().with(AlephaWebSocket);
147
-
148
- class Controller {
149
- ch = $channel({
150
- path: "/ws/multi-room",
151
- schema: { in: chatInSchema, out: chatOutSchema },
152
- });
153
-
154
- ws = $websocket({
155
- channel: this.ch,
156
- handler: async () => {},
157
- onConnect: ({ roomIds: ids }) => {
158
- roomIds.push(...ids);
159
- },
160
- });
161
- }
162
-
163
- alepha.inject(Controller);
164
- await alepha.start();
165
-
166
- const hostname = alepha
167
- .inject(NodeHttpServerProvider)
168
- .hostname.replace("http://", "ws://");
169
-
170
- const ws = new WebSocket(`${hostname}/ws/multi-room?roomIds=a,b,c`);
171
- await waitForOpen(ws);
172
- await delay(100);
173
-
174
- expect(roomIds).toEqual(["a", "b", "c"]);
175
-
176
- ws.close();
177
- await alepha.stop();
178
- });
179
-
180
- test("connection cleanup removes from room manager", async ({ expect }) => {
181
- const alepha = Alepha.create().with(AlephaWebSocket);
182
-
183
- class Controller {
184
- ch = $channel({
185
- path: "/ws/cleanup",
186
- schema: { in: chatInSchema, out: chatOutSchema },
187
- });
188
-
189
- ws = $websocket({
190
- channel: this.ch,
191
- handler: async () => {},
192
- });
193
- }
194
-
195
- alepha.inject(Controller);
196
- await alepha.start();
197
-
198
- const hostname = alepha
199
- .inject(NodeHttpServerProvider)
200
- .hostname.replace("http://", "ws://");
201
- const roomManager = alepha.inject(RoomManager);
202
-
203
- const ws = new WebSocket(`${hostname}/ws/cleanup?roomId=test-room`);
204
- await waitForOpen(ws);
205
- await delay(100);
206
-
207
- expect(roomManager.getRoomConnections("test-room")).toHaveLength(1);
208
-
209
- ws.close();
210
- await delay(100);
211
-
212
- expect(roomManager.getRoomConnections("test-room")).toHaveLength(0);
213
-
214
- await alepha.stop();
215
- });
216
- });
217
-
218
- // -------------------------------------------------------------------------------------------------------------------
219
- // Message handling
220
- // -------------------------------------------------------------------------------------------------------------------
221
-
222
- describe("message handling", () => {
223
- test("handler receives correct context", async ({ expect }) => {
224
- let receivedContext: any = null;
225
-
226
- const alepha = Alepha.create().with(AlephaWebSocket);
227
-
228
- class Controller {
229
- ch = $channel({
230
- path: "/ws/context",
231
- schema: { in: chatInSchema, out: chatOutSchema },
232
- });
233
-
234
- ws = $websocket({
235
- channel: this.ch,
236
- handler: async (ctx) => {
237
- receivedContext = {
238
- connectionId: ctx.connectionId,
239
- roomId: ctx.roomId,
240
- message: ctx.message,
241
- hasReply: typeof ctx.reply === "function",
242
- };
243
- },
244
- });
245
- }
246
-
247
- alepha.inject(Controller);
248
- await alepha.start();
249
-
250
- const hostname = alepha
251
- .inject(NodeHttpServerProvider)
252
- .hostname.replace("http://", "ws://");
253
-
254
- const ws = new WebSocket(`${hostname}/ws/context?roomId=lobby`);
255
- await waitForOpen(ws);
256
- await delay(50);
257
-
258
- ws.send(
259
- JSON.stringify({ roomId: "lobby", message: { content: "hello" } }),
260
- );
261
- await delay(200);
262
-
263
- expect(receivedContext).not.toBeNull();
264
- expect(receivedContext.connectionId).toMatch(/^ws-\d+$/);
265
- expect(receivedContext.roomId).toBe("lobby");
266
- expect(receivedContext.message).toEqual({ content: "hello" });
267
- expect(receivedContext.hasReply).toBe(true);
268
-
269
- ws.close();
270
- await alepha.stop();
271
- });
272
-
273
- test("invalid message returns error to client", async ({ expect }) => {
274
- const alepha = Alepha.create().with(AlephaWebSocket);
275
-
276
- class Controller {
277
- ch = $channel({
278
- path: "/ws/validate",
279
- schema: { in: chatInSchema, out: chatOutSchema },
280
- });
281
-
282
- ws = $websocket({
283
- channel: this.ch,
284
- handler: async () => {},
285
- });
286
- }
287
-
288
- alepha.inject(Controller);
289
- await alepha.start();
290
-
291
- const hostname = alepha
292
- .inject(NodeHttpServerProvider)
293
- .hostname.replace("http://", "ws://");
294
-
295
- const ws = new WebSocket(`${hostname}/ws/validate?roomId=test`);
296
- await waitForOpen(ws);
297
- await delay(50);
298
-
299
- // Send message that doesn't match the out schema (missing "content")
300
- const errorPromise = waitForMessage(ws);
301
- ws.send(
302
- JSON.stringify({ roomId: "test", message: { invalid: "field" } }),
303
- );
304
-
305
- const response = await errorPromise;
306
- expect(response.error).toBeDefined();
307
-
308
- ws.close();
309
- await alepha.stop();
310
- });
311
-
312
- test("non-JSON message is ignored", async ({ expect }) => {
313
- let handlerCalled = false;
314
-
315
- const alepha = Alepha.create().with(AlephaWebSocket);
316
-
317
- class Controller {
318
- ch = $channel({
319
- path: "/ws/nonjson",
320
- schema: { in: chatInSchema, out: chatOutSchema },
321
- });
322
-
323
- ws = $websocket({
324
- channel: this.ch,
325
- handler: async () => {
326
- handlerCalled = true;
327
- },
328
- });
329
- }
330
-
331
- alepha.inject(Controller);
332
- await alepha.start();
333
-
334
- const hostname = alepha
335
- .inject(NodeHttpServerProvider)
336
- .hostname.replace("http://", "ws://");
337
-
338
- const ws = new WebSocket(`${hostname}/ws/nonjson?roomId=test`);
339
- await waitForOpen(ws);
340
- await delay(50);
341
-
342
- ws.send("not json {{{");
343
- await delay(200);
344
-
345
- expect(handlerCalled).toBe(false);
346
-
347
- ws.close();
348
- await alepha.stop();
349
- });
350
- });
351
-
352
- // -------------------------------------------------------------------------------------------------------------------
353
- // Server emit targeting
354
- // -------------------------------------------------------------------------------------------------------------------
355
-
356
- describe("emit targeting", () => {
357
- test("emit to all connections (no targeting)", async ({ expect }) => {
358
- const alepha = Alepha.create().with(AlephaWebSocket);
359
-
360
- class Controller {
361
- ch = $channel({
362
- path: "/ws/broadcast-all",
363
- schema: { in: chatInSchema, out: chatOutSchema },
364
- });
365
-
366
- ws = $websocket({
367
- channel: this.ch,
368
- handler: async () => {},
369
- });
370
- }
371
-
372
- const controller = alepha.inject(Controller);
373
- await alepha.start();
374
-
375
- const hostname = alepha
376
- .inject(NodeHttpServerProvider)
377
- .hostname.replace("http://", "ws://");
378
-
379
- const ws1 = new WebSocket(`${hostname}/ws/broadcast-all?roomId=a`);
380
- const ws2 = new WebSocket(`${hostname}/ws/broadcast-all?roomId=b`);
381
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
382
-
383
- const msgs1: any[] = [];
384
- const msgs2: any[] = [];
385
- collectMessages(ws1, msgs1);
386
- collectMessages(ws2, msgs2);
387
- await delay(50);
388
-
389
- // Emit with no room/user/connection targeting → goes to all
390
- await controller.ws.emit({
391
- message: { type: "announce", content: "global" },
392
- });
393
- await delay(200);
394
-
395
- expect(msgs1).toHaveLength(1);
396
- expect(msgs2).toHaveLength(1);
397
- expect(msgs1[0].content).toBe("global");
398
- expect(msgs2[0].content).toBe("global");
399
-
400
- ws1.close();
401
- ws2.close();
402
- await alepha.stop();
403
- });
404
-
405
- test("emit to specific room only", async ({ expect }) => {
406
- const alepha = Alepha.create().with(AlephaWebSocket);
407
-
408
- class Controller {
409
- ch = $channel({
410
- path: "/ws/room-target",
411
- schema: { in: chatInSchema, out: chatOutSchema },
412
- });
413
-
414
- ws = $websocket({
415
- channel: this.ch,
416
- handler: async () => {},
417
- });
418
- }
419
-
420
- const controller = alepha.inject(Controller);
421
- await alepha.start();
422
-
423
- const hostname = alepha
424
- .inject(NodeHttpServerProvider)
425
- .hostname.replace("http://", "ws://");
426
-
427
- const wsA = new WebSocket(`${hostname}/ws/room-target?roomId=room-a`);
428
- const wsB = new WebSocket(`${hostname}/ws/room-target?roomId=room-b`);
429
- await Promise.all([waitForOpen(wsA), waitForOpen(wsB)]);
430
-
431
- const msgsA: any[] = [];
432
- const msgsB: any[] = [];
433
- collectMessages(wsA, msgsA);
434
- collectMessages(wsB, msgsB);
435
- await delay(50);
436
-
437
- await controller.ws.emit({
438
- roomId: "room-a",
439
- message: { type: "targeted", content: "for room-a" },
440
- });
441
- await delay(200);
442
-
443
- expect(msgsA).toHaveLength(1);
444
- expect(msgsA[0].content).toBe("for room-a");
445
- expect(msgsB).toHaveLength(0);
446
-
447
- wsA.close();
448
- wsB.close();
449
- await alepha.stop();
450
- });
451
-
452
- test("emit to multiple rooms", async ({ expect }) => {
453
- const alepha = Alepha.create().with(AlephaWebSocket);
454
-
455
- class Controller {
456
- ch = $channel({
457
- path: "/ws/multi-room-emit",
458
- schema: { in: chatInSchema, out: chatOutSchema },
459
- });
460
-
461
- ws = $websocket({
462
- channel: this.ch,
463
- handler: async () => {},
464
- });
465
- }
466
-
467
- const controller = alepha.inject(Controller);
468
- await alepha.start();
469
-
470
- const hostname = alepha
471
- .inject(NodeHttpServerProvider)
472
- .hostname.replace("http://", "ws://");
473
-
474
- const ws1 = new WebSocket(`${hostname}/ws/multi-room-emit?roomId=room-1`);
475
- const ws2 = new WebSocket(`${hostname}/ws/multi-room-emit?roomId=room-2`);
476
- const ws3 = new WebSocket(`${hostname}/ws/multi-room-emit?roomId=room-3`);
477
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2), waitForOpen(ws3)]);
478
-
479
- const msgs1: any[] = [];
480
- const msgs2: any[] = [];
481
- const msgs3: any[] = [];
482
- collectMessages(ws1, msgs1);
483
- collectMessages(ws2, msgs2);
484
- collectMessages(ws3, msgs3);
485
- await delay(50);
486
-
487
- await controller.ws.emit({
488
- roomIds: ["room-1", "room-2"],
489
- message: { type: "multi", content: "for 1 and 2" },
490
- });
491
- await delay(200);
492
-
493
- expect(msgs1).toHaveLength(1);
494
- expect(msgs2).toHaveLength(1);
495
- expect(msgs3).toHaveLength(0);
496
-
497
- ws1.close();
498
- ws2.close();
499
- ws3.close();
500
- await alepha.stop();
501
- });
502
-
503
- test("emit with exceptConnectionIds", async ({ expect }) => {
504
- const alepha = Alepha.create().with(AlephaWebSocket);
505
- let capturedConnId = "";
506
-
507
- class Controller {
508
- ch = $channel({
509
- path: "/ws/except-conn",
510
- schema: { in: chatInSchema, out: chatOutSchema },
511
- });
512
-
513
- ws = $websocket({
514
- channel: this.ch,
515
- handler: async () => {},
516
- onConnect: ({ connectionId }) => {
517
- // Capture the first connection ID
518
- if (!capturedConnId) capturedConnId = connectionId;
519
- },
520
- });
521
- }
522
-
523
- const controller = alepha.inject(Controller);
524
- await alepha.start();
525
-
526
- const hostname = alepha
527
- .inject(NodeHttpServerProvider)
528
- .hostname.replace("http://", "ws://");
529
-
530
- const ws1 = new WebSocket(`${hostname}/ws/except-conn?roomId=room`);
531
- await waitForOpen(ws1);
532
- await delay(100); // ensure server-side onConnect fires for ws1 first
533
-
534
- const ws2 = new WebSocket(`${hostname}/ws/except-conn?roomId=room`);
535
- await waitForOpen(ws2);
536
- await delay(100);
537
-
538
- const msgs1: any[] = [];
539
- const msgs2: any[] = [];
540
- collectMessages(ws1, msgs1);
541
- collectMessages(ws2, msgs2);
542
-
543
- // Exclude ws1 from the broadcast
544
- await controller.ws.emit({
545
- roomId: "room",
546
- exceptConnectionIds: [capturedConnId],
547
- message: { type: "selective", content: "not for ws1" },
548
- });
549
- await delay(200);
550
-
551
- expect(msgs1).toHaveLength(0);
552
- expect(msgs2).toHaveLength(1);
553
-
554
- ws1.close();
555
- ws2.close();
556
- await alepha.stop();
557
- });
558
-
559
- test("emit to specific connectionId", async ({ expect }) => {
560
- const alepha = Alepha.create().with(AlephaWebSocket);
561
- let targetConnId = "";
562
-
563
- class Controller {
564
- ch = $channel({
565
- path: "/ws/conn-target",
566
- schema: { in: chatInSchema, out: chatOutSchema },
567
- });
568
-
569
- ws = $websocket({
570
- channel: this.ch,
571
- handler: async () => {},
572
- onConnect: ({ connectionId }) => {
573
- targetConnId = connectionId;
574
- },
575
- });
576
- }
577
-
578
- const controller = alepha.inject(Controller);
579
- await alepha.start();
580
-
581
- const hostname = alepha
582
- .inject(NodeHttpServerProvider)
583
- .hostname.replace("http://", "ws://");
584
-
585
- const ws1 = new WebSocket(`${hostname}/ws/conn-target?roomId=room`);
586
- await waitForOpen(ws1);
587
- await delay(100);
588
-
589
- const savedConnId = targetConnId;
590
-
591
- const ws2 = new WebSocket(`${hostname}/ws/conn-target?roomId=room`);
592
- await waitForOpen(ws2);
593
- await delay(50);
594
-
595
- const msgs1: any[] = [];
596
- const msgs2: any[] = [];
597
- collectMessages(ws1, msgs1);
598
- collectMessages(ws2, msgs2);
599
-
600
- // Emit only to ws1's connectionId
601
- await controller.ws.emit({
602
- connectionId: savedConnId,
603
- message: { type: "direct", content: "only for ws1" },
604
- });
605
- await delay(200);
606
-
607
- expect(msgs1).toHaveLength(1);
608
- expect(msgs1[0].content).toBe("only for ws1");
609
- expect(msgs2).toHaveLength(0);
610
-
611
- ws1.close();
612
- ws2.close();
613
- await alepha.stop();
614
- });
615
- });
616
-
617
- // -------------------------------------------------------------------------------------------------------------------
618
- // Reply from handler
619
- // -------------------------------------------------------------------------------------------------------------------
620
-
621
- describe("reply from handler", () => {
622
- test("reply broadcasts to room", async ({ expect }) => {
623
- const alepha = Alepha.create().with(AlephaWebSocket);
624
-
625
- class Controller {
626
- ch = $channel({
627
- path: "/ws/reply-broadcast",
628
- schema: { in: chatInSchema, out: chatOutSchema },
629
- });
630
-
631
- ws = $websocket({
632
- channel: this.ch,
633
- handler: async ({ message, reply }) => {
634
- await reply({
635
- message: { type: "echo", content: message.content },
636
- });
637
- },
638
- });
639
- }
640
-
641
- alepha.inject(Controller);
642
- await alepha.start();
643
-
644
- const hostname = alepha
645
- .inject(NodeHttpServerProvider)
646
- .hostname.replace("http://", "ws://");
647
-
648
- const ws1 = new WebSocket(`${hostname}/ws/reply-broadcast?roomId=room`);
649
- const ws2 = new WebSocket(`${hostname}/ws/reply-broadcast?roomId=room`);
650
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
651
-
652
- const msgs1: any[] = [];
653
- const msgs2: any[] = [];
654
- collectMessages(ws1, msgs1);
655
- collectMessages(ws2, msgs2);
656
- await delay(50);
657
-
658
- ws1.send(JSON.stringify({ roomId: "room", message: { content: "hi" } }));
659
- await delay(200);
660
-
661
- // Both should receive the reply (no exceptSelf)
662
- expect(msgs1).toHaveLength(1);
663
- expect(msgs2).toHaveLength(1);
664
- expect(msgs1[0].content).toBe("hi");
665
-
666
- ws1.close();
667
- ws2.close();
668
- await alepha.stop();
669
- });
670
-
671
- test("reply with exceptSelf excludes sender", async ({ expect }) => {
672
- const alepha = Alepha.create().with(AlephaWebSocket);
673
-
674
- class Controller {
675
- ch = $channel({
676
- path: "/ws/reply-except",
677
- schema: { in: chatInSchema, out: chatOutSchema },
678
- });
679
-
680
- ws = $websocket({
681
- channel: this.ch,
682
- handler: async ({ message, reply }) => {
683
- await reply({
684
- message: { type: "echo", content: message.content },
685
- exceptSelf: true,
686
- });
687
- },
688
- });
689
- }
690
-
691
- alepha.inject(Controller);
692
- await alepha.start();
693
-
694
- const hostname = alepha
695
- .inject(NodeHttpServerProvider)
696
- .hostname.replace("http://", "ws://");
697
-
698
- const ws1 = new WebSocket(`${hostname}/ws/reply-except?roomId=room`);
699
- const ws2 = new WebSocket(`${hostname}/ws/reply-except?roomId=room`);
700
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
701
-
702
- const msgs1: any[] = [];
703
- const msgs2: any[] = [];
704
- collectMessages(ws1, msgs1);
705
- collectMessages(ws2, msgs2);
706
- await delay(50);
707
-
708
- ws1.send(
709
- JSON.stringify({ roomId: "room", message: { content: "hello" } }),
710
- );
711
- await delay(200);
712
-
713
- // ws1 (sender) should NOT receive, ws2 should
714
- expect(msgs1).toHaveLength(0);
715
- expect(msgs2).toHaveLength(1);
716
- expect(msgs2[0].content).toBe("hello");
717
-
718
- ws1.close();
719
- ws2.close();
720
- await alepha.stop();
721
- });
722
-
723
- test("reply to a different room", async ({ expect }) => {
724
- const alepha = Alepha.create().with(AlephaWebSocket);
725
-
726
- class Controller {
727
- ch = $channel({
728
- path: "/ws/reply-room",
729
- schema: { in: chatInSchema, out: chatOutSchema },
730
- });
731
-
732
- ws = $websocket({
733
- channel: this.ch,
734
- handler: async ({ message, reply }) => {
735
- // Reply to room-b instead of the sender's room
736
- await reply({
737
- roomId: "room-b",
738
- message: { type: "forwarded", content: message.content },
739
- });
740
- },
741
- });
742
- }
743
-
744
- alepha.inject(Controller);
745
- await alepha.start();
746
-
747
- const hostname = alepha
748
- .inject(NodeHttpServerProvider)
749
- .hostname.replace("http://", "ws://");
750
-
751
- const wsA = new WebSocket(`${hostname}/ws/reply-room?roomId=room-a`);
752
- const wsB = new WebSocket(`${hostname}/ws/reply-room?roomId=room-b`);
753
- await Promise.all([waitForOpen(wsA), waitForOpen(wsB)]);
754
-
755
- const msgsA: any[] = [];
756
- const msgsB: any[] = [];
757
- collectMessages(wsA, msgsA);
758
- collectMessages(wsB, msgsB);
759
- await delay(50);
760
-
761
- wsA.send(
762
- JSON.stringify({
763
- roomId: "room-a",
764
- message: { content: "forward me" },
765
- }),
766
- );
767
- await delay(200);
768
-
769
- // room-a sender should not receive, room-b should
770
- expect(msgsA).toHaveLength(0);
771
- expect(msgsB).toHaveLength(1);
772
- expect(msgsB[0].content).toBe("forward me");
773
-
774
- wsA.close();
775
- wsB.close();
776
- await alepha.stop();
777
- });
778
- });
779
-
780
- // -------------------------------------------------------------------------------------------------------------------
781
- // Provider API
782
- // -------------------------------------------------------------------------------------------------------------------
783
-
784
- describe("provider API", () => {
785
- test("getConnections returns all active connections", async ({
786
- expect,
787
- }) => {
788
- const alepha = Alepha.create().with(AlephaWebSocket);
789
-
790
- class Controller {
791
- ch = $channel({
792
- path: "/ws/get-conns",
793
- schema: { in: chatInSchema, out: chatOutSchema },
794
- });
795
-
796
- ws = $websocket({
797
- channel: this.ch,
798
- handler: async () => {},
799
- });
800
- }
801
-
802
- alepha.inject(Controller);
803
- await alepha.start();
804
-
805
- const hostname = alepha
806
- .inject(NodeHttpServerProvider)
807
- .hostname.replace("http://", "ws://");
808
- const provider = alepha.inject(NodeWebSocketServerProvider);
809
-
810
- const ws1 = new WebSocket(`${hostname}/ws/get-conns?roomId=room`);
811
- const ws2 = new WebSocket(`${hostname}/ws/get-conns?roomId=room`);
812
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
813
- await delay(50);
814
-
815
- expect(provider.getConnections()).toHaveLength(2);
816
-
817
- ws1.close();
818
- await delay(100);
819
-
820
- expect(provider.getConnections()).toHaveLength(1);
821
-
822
- ws2.close();
823
- await delay(100);
824
-
825
- expect(provider.getConnections()).toHaveLength(0);
826
-
827
- await alepha.stop();
828
- });
829
-
830
- test("getRoomConnections returns connections in specific room", async ({
831
- expect,
832
- }) => {
833
- const alepha = Alepha.create().with(AlephaWebSocket);
834
-
835
- class Controller {
836
- ch = $channel({
837
- path: "/ws/room-conns",
838
- schema: { in: chatInSchema, out: chatOutSchema },
839
- });
840
-
841
- ws = $websocket({
842
- channel: this.ch,
843
- handler: async () => {},
844
- });
845
- }
846
-
847
- alepha.inject(Controller);
848
- await alepha.start();
849
-
850
- const hostname = alepha
851
- .inject(NodeHttpServerProvider)
852
- .hostname.replace("http://", "ws://");
853
- const provider = alepha.inject(NodeWebSocketServerProvider);
854
-
855
- const ws1 = new WebSocket(`${hostname}/ws/room-conns?roomId=vip`);
856
- const ws2 = new WebSocket(`${hostname}/ws/room-conns?roomId=general`);
857
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
858
- await delay(50);
859
-
860
- expect(provider.getRoomConnections("vip")).toHaveLength(1);
861
- expect(provider.getRoomConnections("general")).toHaveLength(1);
862
- expect(provider.getRoomConnections("nonexistent")).toHaveLength(0);
863
-
864
- ws1.close();
865
- ws2.close();
866
- await alepha.stop();
867
- });
868
-
869
- test("closeConnection closes a specific connection", async ({ expect }) => {
870
- const alepha = Alepha.create().with(AlephaWebSocket);
871
- let connId = "";
872
-
873
- class Controller {
874
- ch = $channel({
875
- path: "/ws/close-conn",
876
- schema: { in: chatInSchema, out: chatOutSchema },
877
- });
878
-
879
- ws = $websocket({
880
- channel: this.ch,
881
- handler: async () => {},
882
- onConnect: ({ connectionId }) => {
883
- connId = connectionId;
884
- },
885
- });
886
- }
887
-
888
- alepha.inject(Controller);
889
- await alepha.start();
890
-
891
- const hostname = alepha
892
- .inject(NodeHttpServerProvider)
893
- .hostname.replace("http://", "ws://");
894
- const provider = alepha.inject(NodeWebSocketServerProvider);
895
-
896
- const ws = new WebSocket(`${hostname}/ws/close-conn?roomId=room`);
897
- await waitForOpen(ws);
898
- await delay(100);
899
-
900
- const closePromise = waitForClose(ws);
901
- await provider.closeConnection(connId, 4000, "kicked");
902
-
903
- const { code } = await closePromise;
904
- expect(code).toBe(4000);
905
-
906
- await alepha.stop();
907
- });
908
- });
909
-
910
- // -------------------------------------------------------------------------------------------------------------------
911
- // Graceful shutdown
912
- // -------------------------------------------------------------------------------------------------------------------
913
-
914
- describe("graceful shutdown", () => {
915
- test("stop closes all connections", async ({ expect }) => {
916
- const alepha = Alepha.create().with(AlephaWebSocket);
917
-
918
- class Controller {
919
- ch = $channel({
920
- path: "/ws/shutdown",
921
- schema: { in: chatInSchema, out: chatOutSchema },
922
- });
923
-
924
- ws = $websocket({
925
- channel: this.ch,
926
- handler: async () => {},
927
- });
928
- }
929
-
930
- alepha.inject(Controller);
931
- await alepha.start();
932
-
933
- const hostname = alepha
934
- .inject(NodeHttpServerProvider)
935
- .hostname.replace("http://", "ws://");
936
-
937
- const ws1 = new WebSocket(`${hostname}/ws/shutdown?roomId=room`);
938
- const ws2 = new WebSocket(`${hostname}/ws/shutdown?roomId=room`);
939
- await Promise.all([waitForOpen(ws1), waitForOpen(ws2)]);
940
-
941
- const close1 = waitForClose(ws1);
942
- const close2 = waitForClose(ws2);
943
-
944
- await alepha.stop();
945
-
946
- const [result1, result2] = await Promise.all([close1, close2]);
947
- expect(result1.code).toBe(1001);
948
- expect(result2.code).toBe(1001);
949
- });
950
- });
951
- });