alepha 0.20.6 → 0.20.8

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 +259 -250
  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 +857 -841
  41. package/dist/api/users/index.d.ts.map +1 -1
  42. package/dist/api/verifications/index.d.ts +128 -127
  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 +116 -132
  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,642 +0,0 @@
1
- import {
2
- $env,
3
- $inject,
4
- Alepha,
5
- AlephaError,
6
- SchemaValidator,
7
- type Static,
8
- t,
9
- } from "alepha";
10
- import { $logger } from "alepha/logger";
11
- import type { ChannelPrimitive, TWSObject } from "../primitives/$channel.ts";
12
-
13
- const envSchema = t.object({
14
- WEBSOCKET_URL: t.text({
15
- default: "",
16
- description:
17
- "WebSocket server URL (e.g., ws://localhost:3001). Leave empty to auto-detect.",
18
- }),
19
- WEBSOCKET_RECONNECT_INTERVAL: t.integer({
20
- default: 3000,
21
- description: "Reconnection interval in milliseconds",
22
- }),
23
- WEBSOCKET_MAX_RECONNECT_ATTEMPTS: t.integer({
24
- default: 10,
25
- description:
26
- "Maximum number of reconnection attempts. Set to -1 for infinite.",
27
- }),
28
- });
29
-
30
- declare module "alepha" {
31
- interface Env extends Partial<Static<typeof envSchema>> {}
32
- }
33
-
34
- /**
35
- * Room subscription
36
- */
37
- interface RoomSubscription<TClient extends TWSObject> {
38
- roomId: string;
39
- handler: (message: Static<TClient>) => void;
40
- }
41
-
42
- /**
43
- * WebSocket channel connection
44
- *
45
- * Manages a single WebSocket connection to a channel with multiple room subscriptions.
46
- * One connection can handle multiple rooms on the same channel.
47
- */
48
- export class WebSocketChannelConnection<
49
- TClient extends TWSObject,
50
- TServer extends TWSObject,
51
- > {
52
- protected readonly alepha = $inject(Alepha);
53
- protected readonly schemaValidator = $inject(SchemaValidator);
54
- protected readonly log = $logger();
55
- protected ws?: WebSocket;
56
- protected reconnectAttempts = 0;
57
- protected reconnectTimer?: number;
58
- protected static readonly MAX_QUEUE_SIZE = 1000;
59
- protected messageQueue: Array<{ roomId: string; message: Static<TServer> }> =
60
- [];
61
-
62
- // Room subscriptions: Map<roomId, handler>
63
- protected subscriptions = new Map<
64
- string,
65
- (message: Static<TClient>) => void
66
- >();
67
-
68
- // Connection state
69
- public isConnected = false;
70
- public isConnecting = false;
71
- public isError = false;
72
- public error?: Error;
73
- protected connectPromise?: Promise<void>;
74
-
75
- // Connection callbacks
76
- protected onConnectCallbacks = new Set<() => void>();
77
- protected onDisconnectCallbacks = new Set<() => void>();
78
- protected onErrorCallbacks = new Set<(error: Error) => void>();
79
-
80
- constructor(
81
- protected readonly channel: ChannelPrimitive<TClient, TServer>,
82
- protected readonly options: {
83
- url?: string;
84
- autoReconnect?: boolean;
85
- reconnectInterval?: number;
86
- maxReconnectAttempts?: number;
87
- },
88
- protected readonly env: Static<typeof envSchema>,
89
- ) {}
90
-
91
- /**
92
- * Build WebSocket URL
93
- */
94
- protected buildUrl(): string {
95
- this.log.trace("Building WebSocket URL", {
96
- hasCustomUrl: !!this.options.url,
97
- channelPath: this.channel.options.path,
98
- });
99
-
100
- if (this.options.url) {
101
- this.log.debug("Using custom WebSocket URL", { url: this.options.url });
102
- return this.options.url;
103
- }
104
-
105
- // Auto-detect URL from current location (browser only)
106
- if (typeof window !== "undefined") {
107
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
108
- const host = window.location.host;
109
- const path = this.channel.options.path;
110
- // Send all room IDs as query params
111
- const roomIds = Array.from(this.subscriptions.keys());
112
- const roomParam =
113
- roomIds.length > 0 ? `?roomIds=${roomIds.join(",")}` : "";
114
- const url = `${protocol}//${host}${path}${roomParam}`;
115
- this.log.debug("Auto-detected WebSocket URL", { url, roomIds });
116
- return url;
117
- }
118
-
119
- // Fallback to env URL
120
- const url = `${this.env.WEBSOCKET_URL}${this.channel.options.path}`;
121
- this.log.debug("Using env WebSocket URL", { url });
122
- return url;
123
- }
124
-
125
- /**
126
- * Subscribe to a room on this channel
127
- */
128
- public subscribe(
129
- roomId: string,
130
- handler: (message: Static<TClient>) => void,
131
- callbacks?: {
132
- onConnect?: () => void;
133
- onDisconnect?: () => void;
134
- onError?: (error: Error) => void;
135
- },
136
- ): () => void {
137
- this.log.debug("Subscribing to room", {
138
- roomId,
139
- channelPath: this.channel.options.path,
140
- existingSubscriptions: this.subscriptions.size,
141
- });
142
-
143
- // Add subscription
144
- this.subscriptions.set(roomId, handler);
145
-
146
- // Add callbacks
147
- if (callbacks?.onConnect) this.onConnectCallbacks.add(callbacks.onConnect);
148
- if (callbacks?.onDisconnect)
149
- this.onDisconnectCallbacks.add(callbacks.onDisconnect);
150
- if (callbacks?.onError) this.onErrorCallbacks.add(callbacks.onError);
151
-
152
- // Connect or reconnect to include the new room in the URL
153
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
154
- this.log.trace("No active connection, initiating connect");
155
- this.connect().catch((error) => {
156
- this.log.error("Failed to connect:", error);
157
- });
158
- } else {
159
- this.log.trace("Reconnecting to include new room subscription", {
160
- roomId,
161
- });
162
- this.reconnect();
163
- }
164
-
165
- // Return unsubscribe function
166
- return () => {
167
- this.log.debug("Unsubscribing from room", { roomId });
168
- this.subscriptions.delete(roomId);
169
- if (callbacks?.onConnect)
170
- this.onConnectCallbacks.delete(callbacks.onConnect);
171
- if (callbacks?.onDisconnect)
172
- this.onDisconnectCallbacks.delete(callbacks.onDisconnect);
173
- if (callbacks?.onError) this.onErrorCallbacks.delete(callbacks.onError);
174
-
175
- // Disconnect if no more subscriptions
176
- if (this.subscriptions.size === 0) {
177
- this.log.debug("No more subscriptions, disconnecting");
178
- this.disconnect();
179
- }
180
- };
181
- }
182
-
183
- /**
184
- * Connect to WebSocket server
185
- */
186
- protected async connect(): Promise<void> {
187
- if (this.ws?.readyState === WebSocket.OPEN) {
188
- this.log.trace("Already connected, skipping connect");
189
- return;
190
- }
191
-
192
- if (this.connectPromise) {
193
- this.log.trace("Connection already in progress, reusing promise");
194
- return this.connectPromise;
195
- }
196
-
197
- this.isConnecting = true;
198
- this.isError = false;
199
- this.error = undefined;
200
-
201
- const url = this.buildUrl();
202
- this.log.info("Connecting to WebSocket server", { url });
203
-
204
- this.connectPromise = new Promise<void>((resolve, reject) => {
205
- try {
206
- const ws = new WebSocket(url);
207
- this.ws = ws;
208
-
209
- ws.onopen = () => {
210
- this.isConnected = true;
211
- this.isConnecting = false;
212
- this.isError = false;
213
- this.error = undefined;
214
- this.reconnectAttempts = 0;
215
-
216
- this.log.info("WebSocket connected", {
217
- channelPath: this.channel.options.path,
218
- rooms: Array.from(this.subscriptions.keys()),
219
- });
220
-
221
- // Flush queued messages
222
- if (this.messageQueue.length > 0) {
223
- this.log.debug("Flushing queued messages", {
224
- count: this.messageQueue.length,
225
- });
226
- }
227
- while (this.messageQueue.length > 0) {
228
- const msg = this.messageQueue.shift();
229
- if (msg) {
230
- this.log.trace("Sending queued message", { roomId: msg.roomId });
231
- ws.send(
232
- JSON.stringify({
233
- roomId: msg.roomId,
234
- message: msg.message,
235
- }),
236
- );
237
- }
238
- }
239
-
240
- // Call all connect callbacks
241
- for (const callback of this.onConnectCallbacks) {
242
- callback();
243
- }
244
-
245
- resolve();
246
- };
247
-
248
- ws.onmessage = (event) => {
249
- this.log.trace("Message received", {
250
- dataLength: event.data?.length,
251
- });
252
- this.handleMessage(event.data);
253
- };
254
-
255
- ws.onclose = (event) => {
256
- this.isConnected = false;
257
- this.isConnecting = false;
258
- this.ws = undefined;
259
-
260
- this.log.info("WebSocket disconnected", {
261
- code: event.code,
262
- reason: event.reason,
263
- wasClean: event.wasClean,
264
- });
265
-
266
- // Call all disconnect callbacks
267
- for (const callback of this.onDisconnectCallbacks) {
268
- callback();
269
- }
270
-
271
- // Attempt reconnection
272
- if (this.options.autoReconnect !== false) {
273
- this.scheduleReconnect();
274
- }
275
- };
276
-
277
- ws.onerror = () => {
278
- const err = new Error("WebSocket connection error");
279
- this.isError = true;
280
- this.error = err;
281
- this.isConnecting = false;
282
-
283
- this.log.error("WebSocket error", { url });
284
-
285
- // Call all error callbacks
286
- for (const callback of this.onErrorCallbacks) {
287
- callback(err);
288
- }
289
-
290
- reject(err);
291
- };
292
- } catch (err) {
293
- const error =
294
- err instanceof Error ? err : new Error("Connection failed");
295
- this.isError = true;
296
- this.error = error;
297
- this.isConnecting = false;
298
-
299
- this.log.error("Failed to create WebSocket", { error: error.message });
300
-
301
- // Call all error callbacks
302
- for (const callback of this.onErrorCallbacks) {
303
- callback(error);
304
- }
305
-
306
- reject(error);
307
- }
308
- }).finally(() => {
309
- this.connectPromise = undefined;
310
- });
311
-
312
- return this.connectPromise;
313
- }
314
-
315
- /**
316
- * Handle incoming message
317
- */
318
- protected handleMessage(data: string): void {
319
- try {
320
- const parsed = JSON.parse(data);
321
- this.log.trace("Parsed incoming message", { parsed });
322
-
323
- // Validate incoming message against schema
324
- const inSchema = this.channel.options.schema.in;
325
- this.alepha.codec.validate(inSchema, parsed);
326
-
327
- this.log.debug("Dispatching message to handlers", {
328
- handlerCount: this.subscriptions.size,
329
- });
330
-
331
- // Extract roomId from message if present (server should send it back)
332
- // For now, broadcast to all subscribed rooms
333
- // TODO: Server should include roomId in response
334
- for (const handler of this.subscriptions.values()) {
335
- handler(parsed as Static<TClient>);
336
- }
337
- } catch (err) {
338
- this.log.error("Error handling message:", err);
339
- }
340
- }
341
-
342
- /**
343
- * Send message to a specific room
344
- */
345
- public async send(roomId: string, message: Static<TServer>): Promise<void> {
346
- this.log.trace("Sending message", { roomId, message });
347
-
348
- // Validate outgoing message against schema
349
- const outSchema = this.channel.options.schema.out;
350
- try {
351
- this.schemaValidator.validate(outSchema, message, {
352
- trim: false,
353
- nullToUndefined: false,
354
- deleteUndefined: false,
355
- });
356
- } catch (err) {
357
- this.log.warn("Message validation failed", { error: err });
358
- throw new AlephaError(
359
- `Message validation failed: ${(err as Error).message}`,
360
- );
361
- }
362
-
363
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
364
- if (
365
- this.messageQueue.length >= WebSocketChannelConnection.MAX_QUEUE_SIZE
366
- ) {
367
- this.log.warn("Message queue full, dropping oldest message", {
368
- roomId,
369
- queueSize: this.messageQueue.length,
370
- });
371
- this.messageQueue.shift();
372
- }
373
- this.log.debug("Connection not ready, queuing message", {
374
- roomId,
375
- queueSize: this.messageQueue.length + 1,
376
- });
377
- this.messageQueue.push({ roomId, message });
378
- return;
379
- }
380
-
381
- this.log.debug("Sending message to server", { roomId });
382
- this.ws.send(
383
- JSON.stringify({
384
- roomId,
385
- message,
386
- }),
387
- );
388
- }
389
-
390
- /**
391
- * Schedule reconnection
392
- */
393
- protected scheduleReconnect(): void {
394
- // Clear any pending reconnect timer
395
- if (this.reconnectTimer) {
396
- clearTimeout(this.reconnectTimer);
397
- this.reconnectTimer = undefined;
398
- }
399
-
400
- const maxAttempts =
401
- this.options.maxReconnectAttempts ??
402
- this.env.WEBSOCKET_MAX_RECONNECT_ATTEMPTS ??
403
- 10;
404
- const reconnectInterval =
405
- this.options.reconnectInterval ??
406
- this.env.WEBSOCKET_RECONNECT_INTERVAL ??
407
- 3000;
408
-
409
- if (maxAttempts !== -1 && this.reconnectAttempts >= maxAttempts) {
410
- this.log.warn("Max reconnection attempts reached", {
411
- attempts: this.reconnectAttempts,
412
- maxAttempts,
413
- });
414
- return;
415
- }
416
-
417
- this.reconnectAttempts++;
418
-
419
- this.log.debug("Scheduling reconnection", {
420
- attempt: this.reconnectAttempts,
421
- maxAttempts,
422
- intervalMs: reconnectInterval,
423
- });
424
-
425
- this.reconnectTimer = window.setTimeout(() => {
426
- this.log.info("Reconnecting...", {
427
- attempt: this.reconnectAttempts,
428
- maxAttempts,
429
- });
430
- this.connect().catch((error) => {
431
- this.log.error("Reconnection failed:", error);
432
- });
433
- }, reconnectInterval);
434
- }
435
-
436
- /**
437
- * Disconnect from server
438
- */
439
- public disconnect(): void {
440
- this.log.debug("Disconnecting", {
441
- hasTimer: !!this.reconnectTimer,
442
- hasConnection: !!this.ws,
443
- });
444
-
445
- if (this.reconnectTimer) {
446
- clearTimeout(this.reconnectTimer);
447
- this.reconnectTimer = undefined;
448
- }
449
-
450
- if (this.ws) {
451
- this.ws.close();
452
- this.ws = undefined;
453
- }
454
-
455
- this.isConnected = false;
456
- this.isConnecting = false;
457
- this.connectPromise = undefined;
458
-
459
- this.log.info("Disconnected");
460
- }
461
-
462
- /**
463
- * Reconnect manually
464
- */
465
- public reconnect(): void {
466
- this.log.info("Manual reconnect requested");
467
- this.disconnect();
468
- this.connect().catch((error) => {
469
- this.log.error("Manual reconnection failed:", error);
470
- });
471
- }
472
-
473
- /**
474
- * Check if subscribed to a room
475
- */
476
- public hasRoom(roomId: string): boolean {
477
- return this.subscriptions.has(roomId);
478
- }
479
-
480
- /**
481
- * Get all subscribed rooms
482
- */
483
- public getRooms(): string[] {
484
- return Array.from(this.subscriptions.keys());
485
- }
486
- }
487
-
488
- /**
489
- * WebSocket Client Service
490
- *
491
- * Manages WebSocket connections from the client side (browser).
492
- * One connection per channel, multiple rooms per connection.
493
- */
494
- export class WebSocketClient {
495
- protected readonly log = $logger();
496
- protected readonly alepha = $inject(Alepha);
497
- protected readonly env = $env(envSchema);
498
-
499
- // Map<channelPath, connection>
500
- protected connections = new Map<
501
- string,
502
- WebSocketChannelConnection<any, any>
503
- >();
504
-
505
- /**
506
- * Subscribe to a room on a channel
507
- */
508
- public subscribe<TClient extends TWSObject, TServer extends TWSObject>(
509
- roomId: string,
510
- channel: ChannelPrimitive<TClient, TServer>,
511
- handler: (message: Static<TClient>) => void,
512
- options: {
513
- url?: string;
514
- autoReconnect?: boolean;
515
- reconnectInterval?: number;
516
- maxReconnectAttempts?: number;
517
- onConnect?: () => void;
518
- onDisconnect?: () => void;
519
- onError?: (error: Error) => void;
520
- } = {},
521
- ): () => void {
522
- const channelPath = channel.options.path;
523
-
524
- this.log.debug("WebSocketClient.subscribe", {
525
- roomId,
526
- channelPath,
527
- existingConnections: this.connections.size,
528
- });
529
-
530
- // Get or create connection for this channel
531
- let connection = this.connections.get(
532
- channelPath,
533
- ) as WebSocketChannelConnection<TClient, TServer>;
534
-
535
- if (!connection) {
536
- this.log.debug("Creating new connection for channel", { channelPath });
537
- connection = this.alepha.inject(WebSocketChannelConnection, {
538
- lifetime: "transient",
539
- args: [
540
- channel,
541
- {
542
- url: options.url,
543
- autoReconnect: options.autoReconnect,
544
- reconnectInterval: options.reconnectInterval,
545
- maxReconnectAttempts: options.maxReconnectAttempts,
546
- },
547
- this.env,
548
- ],
549
- }) as WebSocketChannelConnection<any, any>;
550
-
551
- this.connections.set(channelPath, connection);
552
- } else {
553
- this.log.trace("Reusing existing connection for channel", {
554
- channelPath,
555
- });
556
- }
557
-
558
- // Subscribe to the room on this connection
559
- const unsubscribe = connection.subscribe(roomId, handler, {
560
- onConnect: options.onConnect,
561
- onDisconnect: options.onDisconnect,
562
- onError: options.onError,
563
- });
564
-
565
- // Return unsubscribe function
566
- return () => {
567
- this.log.debug("WebSocketClient.unsubscribe", { roomId, channelPath });
568
- unsubscribe();
569
-
570
- // Clean up connection if no more rooms
571
- if (connection.getRooms().length === 0) {
572
- this.log.debug("Removing connection for channel (no more rooms)", {
573
- channelPath,
574
- });
575
- this.connections.delete(channelPath);
576
- }
577
- };
578
- }
579
-
580
- /**
581
- * Send message to a room on a channel
582
- */
583
- public async send<TClient extends TWSObject, TServer extends TWSObject>(
584
- roomId: string,
585
- channel: ChannelPrimitive<TClient, TServer>,
586
- message: Static<TServer>,
587
- ): Promise<void> {
588
- const channelPath = channel.options.path;
589
-
590
- this.log.trace("WebSocketClient.send", { roomId, channelPath });
591
-
592
- const connection = this.connections.get(
593
- channelPath,
594
- ) as WebSocketChannelConnection<TClient, TServer>;
595
-
596
- if (!connection) {
597
- this.log.warn("Attempted to send on unsubscribed channel", {
598
- channelPath,
599
- });
600
- throw new AlephaError(
601
- `Not subscribed to channel ${channelPath}. Subscribe first before sending messages.`,
602
- );
603
- }
604
-
605
- await connection.send(roomId, message);
606
- }
607
-
608
- /**
609
- * Get connection for a channel
610
- */
611
- public getConnection<TClient extends TWSObject, TServer extends TWSObject>(
612
- channel: ChannelPrimitive<TClient, TServer>,
613
- ): WebSocketChannelConnection<TClient, TServer> | undefined {
614
- const channelPath = channel.options.path;
615
- const connection = this.connections.get(channelPath) as
616
- | WebSocketChannelConnection<TClient, TServer>
617
- | undefined;
618
-
619
- this.log.trace("WebSocketClient.getConnection", {
620
- channelPath,
621
- found: !!connection,
622
- });
623
-
624
- return connection;
625
- }
626
-
627
- /**
628
- * Disconnect all connections
629
- */
630
- public disconnectAll(): void {
631
- this.log.info("Disconnecting all connections", {
632
- count: this.connections.size,
633
- });
634
-
635
- for (const connection of this.connections.values()) {
636
- connection.disconnect();
637
- }
638
- this.connections.clear();
639
-
640
- this.log.debug("All connections disconnected");
641
- }
642
- }