alepha 0.14.4 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +44 -102
  2. package/dist/api/audits/index.d.ts +331 -443
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +2 -2
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +0 -113
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -3
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +151 -262
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/notifications/index.browser.js +4 -4
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +164 -276
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/notifications/index.js +4 -4
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +265 -377
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.browser.js +1 -2
  21. package/dist/api/users/index.browser.js.map +1 -1
  22. package/dist/api/users/index.d.ts +195 -301
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +203 -184
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts.map +1 -1
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/batch/index.js +1 -2
  29. package/dist/batch/index.js.map +1 -1
  30. package/dist/bucket/index.d.ts.map +1 -1
  31. package/dist/cache/core/index.d.ts.map +1 -1
  32. package/dist/cache/redis/index.d.ts.map +1 -1
  33. package/dist/cache/redis/index.js +2 -2
  34. package/dist/cache/redis/index.js.map +1 -1
  35. package/dist/cli/index.d.ts +5900 -165
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +1481 -639
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +8 -4
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +29 -25
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +563 -54
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +175 -8
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +564 -54
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +563 -54
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts.map +1 -1
  52. package/dist/datetime/index.js +4 -4
  53. package/dist/datetime/index.js.map +1 -1
  54. package/dist/email/index.d.ts +89 -42
  55. package/dist/email/index.d.ts.map +1 -1
  56. package/dist/email/index.js +129 -33
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +7969 -2
  59. package/dist/fake/index.d.ts.map +1 -1
  60. package/dist/fake/index.js +22 -22
  61. package/dist/fake/index.js.map +1 -1
  62. package/dist/file/index.d.ts +134 -1
  63. package/dist/file/index.d.ts.map +1 -1
  64. package/dist/file/index.js +253 -1
  65. package/dist/file/index.js.map +1 -1
  66. package/dist/lock/core/index.d.ts.map +1 -1
  67. package/dist/lock/redis/index.d.ts.map +1 -1
  68. package/dist/logger/index.d.ts +1 -2
  69. package/dist/logger/index.d.ts.map +1 -1
  70. package/dist/logger/index.js +1 -5
  71. package/dist/logger/index.js.map +1 -1
  72. package/dist/mcp/index.d.ts +19 -1
  73. package/dist/mcp/index.d.ts.map +1 -1
  74. package/dist/mcp/index.js +28 -4
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/orm/chunk-DH6iiROE.js +38 -0
  77. package/dist/orm/index.browser.js +9 -9
  78. package/dist/orm/index.browser.js.map +1 -1
  79. package/dist/orm/index.bun.js +2821 -0
  80. package/dist/orm/index.bun.js.map +1 -0
  81. package/dist/orm/index.d.ts +318 -169
  82. package/dist/orm/index.d.ts.map +1 -1
  83. package/dist/orm/index.js +2086 -1776
  84. package/dist/orm/index.js.map +1 -1
  85. package/dist/queue/core/index.d.ts +4 -4
  86. package/dist/queue/core/index.d.ts.map +1 -1
  87. package/dist/queue/redis/index.d.ts.map +1 -1
  88. package/dist/redis/index.bun.js +285 -0
  89. package/dist/redis/index.bun.js.map +1 -0
  90. package/dist/redis/index.d.ts +13 -31
  91. package/dist/redis/index.d.ts.map +1 -1
  92. package/dist/redis/index.js +18 -38
  93. package/dist/redis/index.js.map +1 -1
  94. package/dist/retry/index.d.ts.map +1 -1
  95. package/dist/router/index.d.ts.map +1 -1
  96. package/dist/scheduler/index.d.ts +83 -1
  97. package/dist/scheduler/index.d.ts.map +1 -1
  98. package/dist/scheduler/index.js +393 -1
  99. package/dist/scheduler/index.js.map +1 -1
  100. package/dist/security/index.browser.js +5 -1
  101. package/dist/security/index.browser.js.map +1 -1
  102. package/dist/security/index.d.ts +598 -112
  103. package/dist/security/index.d.ts.map +1 -1
  104. package/dist/security/index.js +1808 -97
  105. package/dist/security/index.js.map +1 -1
  106. package/dist/server/auth/index.d.ts +1200 -175
  107. package/dist/server/auth/index.d.ts.map +1 -1
  108. package/dist/server/auth/index.js +1268 -37
  109. package/dist/server/auth/index.js.map +1 -1
  110. package/dist/server/cache/index.d.ts +6 -3
  111. package/dist/server/cache/index.d.ts.map +1 -1
  112. package/dist/server/cache/index.js +1 -1
  113. package/dist/server/cache/index.js.map +1 -1
  114. package/dist/server/compress/index.d.ts.map +1 -1
  115. package/dist/server/cookies/index.d.ts.map +1 -1
  116. package/dist/server/cookies/index.js +3 -3
  117. package/dist/server/cookies/index.js.map +1 -1
  118. package/dist/server/core/index.d.ts +115 -13
  119. package/dist/server/core/index.d.ts.map +1 -1
  120. package/dist/server/core/index.js +321 -139
  121. package/dist/server/core/index.js.map +1 -1
  122. package/dist/server/cors/index.d.ts +0 -1
  123. package/dist/server/cors/index.d.ts.map +1 -1
  124. package/dist/server/health/index.d.ts +0 -1
  125. package/dist/server/health/index.d.ts.map +1 -1
  126. package/dist/server/helmet/index.d.ts.map +1 -1
  127. package/dist/server/links/index.browser.js +9 -1
  128. package/dist/server/links/index.browser.js.map +1 -1
  129. package/dist/server/links/index.d.ts +1 -2
  130. package/dist/server/links/index.d.ts.map +1 -1
  131. package/dist/server/links/index.js +14 -7
  132. package/dist/server/links/index.js.map +1 -1
  133. package/dist/server/metrics/index.d.ts +514 -1
  134. package/dist/server/metrics/index.d.ts.map +1 -1
  135. package/dist/server/metrics/index.js +4462 -4
  136. package/dist/server/metrics/index.js.map +1 -1
  137. package/dist/server/multipart/index.d.ts.map +1 -1
  138. package/dist/server/proxy/index.d.ts +0 -1
  139. package/dist/server/proxy/index.d.ts.map +1 -1
  140. package/dist/server/rate-limit/index.d.ts.map +1 -1
  141. package/dist/server/static/index.d.ts.map +1 -1
  142. package/dist/server/swagger/index.d.ts +1 -2
  143. package/dist/server/swagger/index.d.ts.map +1 -1
  144. package/dist/server/swagger/index.js +1 -2
  145. package/dist/server/swagger/index.js.map +1 -1
  146. package/dist/sms/index.d.ts +3 -1
  147. package/dist/sms/index.d.ts.map +1 -1
  148. package/dist/sms/index.js +10 -10
  149. package/dist/sms/index.js.map +1 -1
  150. package/dist/thread/index.d.ts +0 -1
  151. package/dist/thread/index.d.ts.map +1 -1
  152. package/dist/thread/index.js +2 -2
  153. package/dist/thread/index.js.map +1 -1
  154. package/dist/topic/core/index.d.ts.map +1 -1
  155. package/dist/topic/redis/index.d.ts.map +1 -1
  156. package/dist/vite/index.d.ts +6315 -149
  157. package/dist/vite/index.d.ts.map +1 -1
  158. package/dist/vite/index.js +140 -469
  159. package/dist/vite/index.js.map +1 -1
  160. package/dist/websocket/index.browser.js +9 -9
  161. package/dist/websocket/index.browser.js.map +1 -1
  162. package/dist/websocket/index.d.ts +28 -28
  163. package/dist/websocket/index.d.ts.map +1 -1
  164. package/dist/websocket/index.js +9 -9
  165. package/dist/websocket/index.js.map +1 -1
  166. package/package.json +13 -18
  167. package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
  168. package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
  169. package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
  170. package/src/api/users/entities/users.ts +1 -1
  171. package/src/api/users/index.ts +8 -8
  172. package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
  173. package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
  174. package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
  175. package/src/api/users/services/CredentialService.ts +7 -7
  176. package/src/api/users/services/IdentityService.ts +4 -4
  177. package/src/api/users/services/RegistrationService.spec.ts +25 -27
  178. package/src/api/users/services/RegistrationService.ts +38 -27
  179. package/src/api/users/services/SessionCrudService.ts +3 -3
  180. package/src/api/users/services/SessionService.spec.ts +3 -3
  181. package/src/api/users/services/SessionService.ts +27 -18
  182. package/src/api/users/services/UserService.ts +7 -7
  183. package/src/batch/providers/BatchProvider.ts +1 -2
  184. package/src/cli/apps/AlephaCli.ts +2 -2
  185. package/src/cli/apps/AlephaPackageBuilderCli.ts +47 -20
  186. package/src/cli/assets/apiHelloControllerTs.ts +19 -0
  187. package/src/cli/assets/apiIndexTs.ts +16 -0
  188. package/src/cli/assets/biomeJson.ts +2 -1
  189. package/src/cli/assets/claudeMd.ts +308 -0
  190. package/src/cli/assets/dummySpecTs.ts +2 -1
  191. package/src/cli/assets/editorconfig.ts +2 -1
  192. package/src/cli/assets/mainBrowserTs.ts +4 -3
  193. package/src/cli/assets/mainCss.ts +24 -0
  194. package/src/cli/assets/mainServerTs.ts +24 -0
  195. package/src/cli/assets/tsconfigJson.ts +2 -1
  196. package/src/cli/assets/webAppRouterTs.ts +16 -0
  197. package/src/cli/assets/webHelloComponentTsx.ts +20 -0
  198. package/src/cli/assets/webIndexTs.ts +16 -0
  199. package/src/cli/atoms/appEntryOptions.ts +13 -0
  200. package/src/cli/atoms/buildOptions.ts +1 -1
  201. package/src/cli/atoms/changelogOptions.ts +1 -1
  202. package/src/cli/commands/build.ts +97 -61
  203. package/src/cli/commands/db.ts +21 -18
  204. package/src/cli/commands/deploy.ts +17 -5
  205. package/src/cli/commands/dev.ts +26 -47
  206. package/src/cli/commands/gen/env.ts +1 -1
  207. package/src/cli/commands/init.ts +79 -25
  208. package/src/cli/commands/lint.ts +9 -3
  209. package/src/cli/commands/test.ts +8 -2
  210. package/src/cli/commands/typecheck.ts +5 -1
  211. package/src/cli/commands/verify.ts +4 -2
  212. package/src/cli/defineConfig.ts +9 -0
  213. package/src/cli/index.ts +2 -1
  214. package/src/cli/providers/AppEntryProvider.ts +131 -0
  215. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  216. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  217. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  218. package/src/cli/services/AlephaCliUtils.ts +72 -602
  219. package/src/cli/services/PackageManagerUtils.ts +308 -0
  220. package/src/cli/services/ProjectScaffolder.ts +329 -0
  221. package/src/command/helpers/Runner.ts +15 -3
  222. package/src/core/Alepha.ts +2 -8
  223. package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
  224. package/src/core/index.shared.ts +1 -0
  225. package/src/core/index.ts +2 -0
  226. package/src/core/primitives/$hook.ts +6 -2
  227. package/src/core/primitives/$module.spec.ts +4 -0
  228. package/src/core/primitives/$module.ts +12 -0
  229. package/src/core/providers/AlsProvider.ts +1 -1
  230. package/src/core/providers/CodecManager.spec.ts +12 -6
  231. package/src/core/providers/CodecManager.ts +26 -6
  232. package/src/core/providers/EventManager.ts +169 -13
  233. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +878 -0
  234. package/src/core/providers/KeylessJsonSchemaCodec.ts +789 -0
  235. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  236. package/src/core/providers/StateManager.spec.ts +27 -16
  237. package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
  238. package/src/email/providers/LocalEmailProvider.ts +52 -15
  239. package/src/email/providers/NodemailerEmailProvider.ts +167 -56
  240. package/src/file/errors/FileError.ts +7 -0
  241. package/src/file/index.ts +9 -1
  242. package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
  243. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  244. package/src/mcp/errors/McpError.ts +30 -0
  245. package/src/mcp/index.ts +3 -0
  246. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  247. package/src/orm/index.browser.ts +1 -19
  248. package/src/orm/index.bun.ts +77 -0
  249. package/src/orm/index.shared-server.ts +22 -0
  250. package/src/orm/index.shared.ts +15 -0
  251. package/src/orm/index.ts +19 -39
  252. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  253. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
  254. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  255. package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
  256. package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
  257. package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
  258. package/src/orm/services/Repository.ts +19 -0
  259. package/src/redis/index.bun.ts +35 -0
  260. package/src/redis/providers/BunRedisProvider.ts +12 -43
  261. package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
  262. package/src/redis/providers/NodeRedisProvider.ts +16 -34
  263. package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
  264. package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
  265. package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
  266. package/src/security/index.browser.ts +5 -0
  267. package/src/security/index.ts +90 -7
  268. package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
  269. package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
  270. package/src/security/primitives/$role.ts +5 -5
  271. package/src/security/primitives/$serviceAccount.spec.ts +5 -5
  272. package/src/security/primitives/$serviceAccount.ts +3 -3
  273. package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
  274. package/src/server/auth/primitives/$auth.ts +10 -10
  275. package/src/server/auth/primitives/$authCredentials.ts +3 -3
  276. package/src/server/auth/primitives/$authGithub.ts +3 -3
  277. package/src/server/auth/primitives/$authGoogle.ts +3 -3
  278. package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
  279. package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
  280. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
  281. package/src/server/core/index.ts +1 -1
  282. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  283. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  284. package/src/server/core/providers/NodeHttpServerProvider.ts +92 -24
  285. package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
  286. package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
  287. package/src/server/core/providers/ServerProvider.ts +144 -24
  288. package/src/server/core/providers/ServerRouterProvider.ts +259 -115
  289. package/src/server/core/providers/ServerTimingProvider.ts +2 -2
  290. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  291. package/src/server/links/index.browser.ts +2 -0
  292. package/src/server/links/index.ts +3 -1
  293. package/src/server/links/providers/LinkProvider.ts +1 -1
  294. package/src/server/swagger/index.ts +1 -1
  295. package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
  296. package/src/sms/providers/LocalSmsProvider.ts +8 -7
  297. package/src/vite/index.ts +3 -2
  298. package/src/vite/tasks/buildClient.ts +0 -1
  299. package/src/vite/tasks/buildServer.ts +80 -22
  300. package/src/vite/tasks/copyAssets.ts +5 -4
  301. package/src/vite/tasks/generateCloudflare.ts +7 -0
  302. package/src/vite/tasks/generateSitemap.ts +64 -23
  303. package/src/vite/tasks/index.ts +0 -2
  304. package/src/vite/tasks/prerenderPages.ts +49 -24
  305. package/dist/server/security/index.browser.js +0 -13
  306. package/dist/server/security/index.browser.js.map +0 -1
  307. package/dist/server/security/index.d.ts +0 -173
  308. package/dist/server/security/index.d.ts.map +0 -1
  309. package/dist/server/security/index.js +0 -311
  310. package/dist/server/security/index.js.map +0 -1
  311. package/src/cli/assets/appRouterTs.ts +0 -9
  312. package/src/cli/assets/indexHtml.ts +0 -15
  313. package/src/cli/assets/mainTs.ts +0 -13
  314. package/src/cli/commands/format.ts +0 -17
  315. package/src/server/security/index.browser.ts +0 -10
  316. package/src/server/security/index.ts +0 -94
  317. package/src/vite/helpers/boot.ts +0 -106
  318. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  319. package/src/vite/tasks/devServer.ts +0 -69
  320. package/src/vite/tasks/runAlepha.ts +0 -270
  321. /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
  322. /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
@@ -1,6 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { Readable as NodeStream } from "node:stream";
2
3
  import { ReadableStream as NodeWebStream } from "node:stream/web";
3
- import { $inject, Alepha, isFileLike, isTypeFile, isUUID, t } from "alepha";
4
+ import type { CompiledEventExecutor, Hooks } from "alepha";
5
+ import { $inject, Alepha, isFileLike, isTypeFile, t } from "alepha";
4
6
  import { $logger } from "alepha/logger";
5
7
  import { RouterProvider } from "alepha/router";
6
8
  import type { RouteMethod } from "../constants/routeMethods.ts";
@@ -19,11 +21,12 @@ import { ServerRequestParser } from "../services/ServerRequestParser.ts";
19
21
  import { ServerTimingProvider } from "./ServerTimingProvider.ts";
20
22
 
21
23
  /**
22
- * Main router for all routes on the server side.
24
+ * Main router for all routes server side.
23
25
  *
26
+ * Reminder:
24
27
  * - $route => generic route
25
28
  * - $action => action route (for API calls)
26
- * - $page => React route (for SSR)
29
+ * - $page => React route (for React SSR)
27
30
  */
28
31
  export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
29
32
  protected readonly log = $logger();
@@ -32,6 +35,66 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
32
35
  protected readonly serverTimingProvider = $inject(ServerTimingProvider);
33
36
  protected readonly serverRequestParser = $inject(ServerRequestParser);
34
37
 
38
+ // Compiled event executors - initialized on first request
39
+ protected compiledOnRequest?: CompiledEventExecutor<
40
+ Hooks["server:onRequest"]
41
+ >;
42
+ protected compiledOnSend?: CompiledEventExecutor<Hooks["server:onSend"]>;
43
+ protected compiledOnResponse?: CompiledEventExecutor<
44
+ Hooks["server:onResponse"]
45
+ >;
46
+ protected compiledOnError?: CompiledEventExecutor<Hooks["server:onError"]>;
47
+
48
+ // Reusable context.run options object - mutated per request
49
+ // Includes slots for request data to avoid closure allocation in context.run
50
+ protected readonly contextRunOptions = {
51
+ context: "",
52
+ // Request data slots - populated before context.run, read by processRequestBound
53
+ _request: null as unknown as ServerRequest,
54
+ _route: null as unknown as ServerRoute,
55
+ _responseKind: "any" as ResponseKind,
56
+ };
57
+
58
+ // Pre-bound method reference - created once at instantiation, reused for all requests
59
+ // Reads arguments from contextRunOptions to avoid closure allocation per request
60
+ protected readonly processRequestBound = (): Promise<any> => {
61
+ const opts = this.contextRunOptions;
62
+ return this.processRequest(opts._request, opts._route, opts._responseKind);
63
+ };
64
+
65
+ // Cache query schema keys to avoid property enumeration on every request
66
+ // WeakMap allows GC of schemas that are no longer referenced
67
+ protected readonly queryKeysCache = new WeakMap<object, string[]>();
68
+
69
+ /**
70
+ * Get cached keys for a query schema, computing them lazily on first access.
71
+ */
72
+ protected getQuerySchemaKeys(schema: { properties: object }): string[] {
73
+ let keys = this.queryKeysCache.get(schema.properties);
74
+ if (!keys) {
75
+ keys = Object.keys(schema.properties);
76
+ this.queryKeysCache.set(schema.properties, keys);
77
+ }
78
+ return keys;
79
+ }
80
+
81
+ /**
82
+ * Compile event executors for optimal performance.
83
+ * Called lazily on first request after all hooks are registered.
84
+ */
85
+ protected compileEvents(): void {
86
+ if (this.compiledOnRequest) return; // Already compiled
87
+
88
+ this.compiledOnRequest = this.alepha.events.compile("server:onRequest");
89
+ this.compiledOnSend = this.alepha.events.compile("server:onSend", {
90
+ catch: true,
91
+ });
92
+ this.compiledOnResponse = this.alepha.events.compile("server:onResponse", {
93
+ catch: true,
94
+ });
95
+ this.compiledOnError = this.alepha.events.compile("server:onError");
96
+ }
97
+
35
98
  /**
36
99
  * Get all registered routes, optionally filtered by a pattern.
37
100
  *
@@ -52,6 +115,9 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
52
115
  return this.routes;
53
116
  }
54
117
 
118
+ /**
119
+ * Create a new server route.
120
+ */
55
121
  public createRoute<TConfig extends RequestConfigSchema = RequestConfigSchema>(
56
122
  route: ServerRoute<TConfig>,
57
123
  ): void {
@@ -71,69 +137,87 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
71
137
  const request =
72
138
  this.serverRequestParser.createServerRequest(rawRequest);
73
139
 
74
- return this.alepha.context.run(
75
- () => this.processRequest(request, route, responseKind),
76
- {
77
- context: this.getContextId(rawRequest.headers),
78
- },
79
- );
140
+ // Populate pre-allocated options with request data
141
+ // This avoids closure allocation - processRequestBound reads from these slots
142
+ const opts = this.contextRunOptions;
143
+ opts.context = this.getContextId(rawRequest.headers);
144
+ opts._request = request;
145
+ opts._route = route;
146
+ opts._responseKind = responseKind;
147
+
148
+ // Use pre-bound method reference instead of creating closure per request
149
+ return this.alepha.context.run(this.processRequestBound, opts);
80
150
  },
81
151
  });
82
152
  }
83
153
 
154
+ /**
155
+ * Get or generate a context ID from request headers.
156
+ */
84
157
  protected getContextId(headers: Record<string, string>): string {
85
- const contextId = headers["x-request-id"] || headers["x-correlation-id"];
86
- // TODO: check if it's not overkill, just checking length might be enough?
87
- // some cloud providers generate non-UUID request ids
88
- if (isUUID(contextId)) {
158
+ // Use constant header names to reduce string allocation
159
+ const contextId =
160
+ headers[HEADER_REQUEST_ID] || headers[HEADER_CORRELATION_ID];
161
+ if (contextId) {
89
162
  return contextId;
90
163
  }
91
164
 
92
- return crypto.randomUUID();
165
+ return randomUUID();
93
166
  }
94
167
 
168
+ /**
169
+ * Process an incoming request through the full lifecycle:
170
+ * - onRequest hooks
171
+ * - route handler
172
+ * - onSend hooks
173
+ * - response serialization
174
+ * - onResponse hooks
175
+ */
95
176
  protected async processRequest(
96
177
  request: ServerRequest,
97
178
  route: ServerRoute,
98
179
  responseKind: ResponseKind,
99
180
  ) {
100
- await this.runRouteHandler(route, request, responseKind).catch((error) => {
101
- return this.errorHandler(route, request, error as Error);
102
- });
181
+ // Compile events on first request (after all hooks are registered)
182
+ this.compileEvents();
103
183
 
104
- await this.alepha.events.emit(
105
- "server:onSend",
106
- {
107
- request,
108
- route,
109
- },
110
- {
111
- catch: true, // avoid unhandled rejection
112
- },
113
- );
184
+ // Use try/catch instead of .catch() to avoid function creation overhead
185
+ try {
186
+ await this.runRouteHandler(route, request, responseKind);
187
+ } catch (error) {
188
+ await this.errorHandler(route, request, error as Error);
189
+ }
190
+
191
+ // Local reference to reduce property lookups
192
+ const reply = request.reply;
193
+
194
+ // Create payload per request to avoid race conditions with concurrent requests
195
+ // (async hooks may hold references while other requests modify shared payloads)
196
+ const payload = { request, route, response: undefined as any };
114
197
 
115
- // create response
198
+ // Use compiled executor - only await if returns promise
199
+ const onSendResult = this.compiledOnSend!(payload);
200
+ if (onSendResult) await onSendResult;
201
+
202
+ // Create response
116
203
  const response = {
117
- status: request.reply.status ?? (request.reply.body ? 200 : 204),
118
- headers: request.reply.headers,
119
- body: request.reply.body as any,
204
+ status: reply.status ?? (reply.body ? 200 : 204),
205
+ headers: reply.headers,
206
+ body: reply.body as any,
120
207
  };
121
208
 
122
- await this.alepha.events.emit(
123
- "server:onResponse",
124
- {
125
- request,
126
- route,
127
- response,
128
- },
129
- {
130
- catch: true, // avoid unhandled rejection
131
- },
132
- );
209
+ payload.response = response;
210
+
211
+ // Use compiled executor - only await if returns promise
212
+ const onResponseResult = this.compiledOnResponse!(payload);
213
+ if (onResponseResult) await onResponseResult;
133
214
 
134
215
  return response;
135
216
  }
136
217
 
218
+ /**
219
+ * Run the route handler with request validation and response serialization.
220
+ */
137
221
  protected async runRouteHandler(
138
222
  route: ServerRoute,
139
223
  request: ServerRequest,
@@ -144,67 +228,73 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
144
228
  // - ServerSecurityProvider (build user from headers)
145
229
  // - ServerLoggerProvider (log request)
146
230
 
147
- await this.alepha.events.emit(
148
- "server:onRequest", // this hook will fill request.user and request.cookies
149
- {
150
- request,
151
- route,
152
- },
153
- {
154
- log: false,
155
- },
156
- );
231
+ // Local reference for timing provider
232
+ const timing = this.serverTimingProvider;
157
233
 
158
- if (
159
- request.reply.body ||
160
- (request.reply.status && request.reply.status >= 200)
161
- ) {
234
+ // Create payload per request to avoid race conditions with concurrent requests
235
+ const payload = { request, route };
236
+
237
+ // Use compiled executor - only await if returns promise
238
+ const onRequestResult = this.compiledOnRequest!(payload);
239
+ if (onRequestResult) await onRequestResult;
240
+
241
+ // Local reference to reduce property lookups
242
+ const reply = request.reply;
243
+ if (reply.body || (reply.status && reply.status >= 200)) {
162
244
  // if the body is already set, we can skip the handler
163
245
  // this is useful for middlewares that set the body
164
246
  return;
165
247
  }
166
248
 
167
249
  // request is ready to be used -> inject to context
168
- this.alepha.context.set<ServerRequest>("request", request as ServerRequest);
250
+ this.alepha.context.set<ServerRequest>(CTX_REQUEST, request);
169
251
 
170
252
  // validate request
171
- this.serverTimingProvider.beginTiming("validateRequest");
253
+ timing.beginTiming(TIMING_VALIDATE);
172
254
  try {
173
255
  this.validateRequest(route, request);
174
256
  } finally {
175
- this.serverTimingProvider.endTiming("validateRequest");
257
+ timing.endTiming(TIMING_VALIDATE);
176
258
  }
177
259
 
178
260
  // call the handler only if the body is not set yet
179
- this.serverTimingProvider.beginTiming("runHandler");
261
+ timing.beginTiming(TIMING_HANDLER);
180
262
  try {
181
263
  const result = await route.handler(request);
182
264
  if (result) {
183
265
  request.reply.body = result;
184
266
  }
185
267
  } finally {
186
- this.serverTimingProvider.endTiming("runHandler");
268
+ timing.endTiming(TIMING_HANDLER);
187
269
  }
188
270
 
189
271
  // serialize response
190
- this.serverTimingProvider.beginTiming("serializeResponse");
272
+ timing.beginTiming(TIMING_SERIALIZE);
191
273
  try {
192
274
  this.serializeResponse(route, request.reply, responseKind);
193
275
  } finally {
194
- this.serverTimingProvider.endTiming("serializeResponse");
276
+ timing.endTiming(TIMING_SERIALIZE);
195
277
  }
196
278
  }
197
279
 
280
+ /**
281
+ * Transform reply body based on response kind and route schema.
282
+ */
198
283
  public serializeResponse(
199
284
  route: ServerRoute,
200
285
  reply: ServerReply,
201
286
  responseKind: ResponseKind,
202
287
  ): void {
288
+ // Local reference to reduce property lookups
289
+ const headers = reply.headers;
290
+
203
291
  if (responseKind === "json" && route.schema?.response) {
204
- reply.headers["content-type"] = "application/json";
205
- reply.body = this.alepha.codec.encode(route.schema.response, reply.body, {
206
- as: "string",
207
- });
292
+ headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
293
+ reply.body = this.alepha.codec.encode(
294
+ route.schema.response,
295
+ reply.body,
296
+ ENCODE_OPTIONS_STRING,
297
+ );
208
298
  return;
209
299
  }
210
300
 
@@ -212,11 +302,11 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
212
302
  if (!isFileLike(reply.body)) {
213
303
  throw new HttpError({
214
304
  status: 500,
215
- message: "Invalid response body - not a file",
305
+ message: ERROR_NOT_FILE,
216
306
  });
217
307
  }
218
- reply.headers["content-type"] = reply.body.type;
219
- reply.headers["content-disposition"] =
308
+ headers[HEADER_CONTENT_TYPE] = reply.body.type;
309
+ headers[HEADER_CONTENT_DISPOSITION] =
220
310
  `attachment; filename="${reply.body.name.replaceAll('"', "")}"`;
221
311
  reply.body = reply.body.stream();
222
312
  return;
@@ -224,22 +314,26 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
224
314
 
225
315
  if (responseKind === "text") {
226
316
  reply.body = String(reply.body);
227
- if (reply.body.startsWith("<!DOCTYPE html>")) {
228
- reply.headers["content-type"] ??= "text/html; charset=UTF-8";
317
+ if (
318
+ reply.body.length > 15 &&
319
+ reply.body.charCodeAt(0) === 60 &&
320
+ reply.body.startsWith("<!DOCTYPE html>")
321
+ ) {
322
+ headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_HTML;
229
323
  } else {
230
- reply.headers["content-type"] ??= "text/plain";
324
+ headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_TEXT;
231
325
  }
232
326
  return;
233
327
  }
234
328
 
235
329
  if (reply.body == null || responseKind === "void") {
236
- delete reply.headers["content-type"];
330
+ delete headers[HEADER_CONTENT_TYPE];
237
331
  reply.body = undefined;
238
332
  return;
239
333
  }
240
334
 
241
335
  if (Buffer.isBuffer(reply.body)) {
242
- reply.headers["content-type"] ??= "application/octet-stream";
336
+ headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_OCTET;
243
337
  return;
244
338
  }
245
339
 
@@ -248,15 +342,18 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
248
342
  reply.body instanceof NodeStream
249
343
  ) {
250
344
  // set content-type to application/octet-stream if not set
251
- reply.headers["content-type"] ??= "application/octet-stream";
345
+ headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_OCTET;
252
346
  return;
253
347
  }
254
348
 
255
- reply.headers["content-type"] ??= "text/plain";
349
+ headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_TEXT;
256
350
  reply.body = String(reply.body);
257
351
  return;
258
352
  }
259
353
 
354
+ /**
355
+ * Determine response type based on route schema.
356
+ */
260
357
  protected getResponseType(schema?: RequestConfigSchema): ResponseKind {
261
358
  if (schema?.response) {
262
359
  if (
@@ -288,68 +385,75 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
288
385
  return "any";
289
386
  }
290
387
 
388
+ /**
389
+ * When an error occurs during request processing, this method is called.
390
+ */
291
391
  protected async errorHandler(
292
392
  route: ServerRoute,
293
393
  request: ServerRequest,
294
394
  error: Error,
295
395
  ) {
296
- // reset body, which is probably invalid,
297
- // it can be filled by server:onError hook or by the default handler below
298
- request.reply.body = null;
299
-
300
- await this.alepha.events.emit(
301
- "server:onError",
302
- {
303
- request,
304
- route,
305
- error,
306
- },
307
- {
308
- log: false,
309
- },
310
- );
396
+ // Local references to reduce property lookups
397
+ const reply = request.reply;
398
+ const headers = reply.headers;
399
+ const requestId = request.requestId;
400
+
401
+ // Reset body, which is probably invalid!
402
+ // It can be filled by server:onError hook or by the default handler below
403
+ reply.body = null;
404
+
405
+ // Use compiled executor - only await if returns promise
406
+ const onErrorResult = this.compiledOnError!({ request, route, error });
407
+ if (onErrorResult) {
408
+ await onErrorResult;
409
+ }
311
410
 
312
- if (!request.reply.body && !request.reply.status) {
411
+ if (!reply.body && !reply.status) {
313
412
  if (error instanceof HttpError) {
314
- request.reply.status = error.status;
315
- request.reply.headers["content-type"] = "application/json";
316
- request.reply.body = JSON.stringify({
317
- ...HttpError.toJSON(error),
318
- requestId: request.requestId,
319
- });
413
+ reply.status = error.status;
414
+ headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
415
+ // Avoid spread operator - directly mutate the error JSON
416
+ const errorJson = HttpError.toJSON(error);
417
+ errorJson.requestId = requestId;
418
+ reply.body = JSON.stringify(errorJson);
320
419
  } else {
321
420
  if (
322
421
  "status" in error &&
323
422
  typeof error.status === "number" &&
324
423
  !!errorNameByStatus[error.status]
325
424
  ) {
326
- request.reply.status = error.status;
327
- request.reply.headers["content-type"] = "application/json";
328
- request.reply.body = JSON.stringify({
329
- status: error.status,
330
- error: errorNameByStatus[error.status],
331
- message: (error as Error).message,
332
- requestId: request.requestId,
425
+ const status = error.status;
426
+ reply.status = status;
427
+ headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
428
+ reply.body = JSON.stringify({
429
+ status,
430
+ error: errorNameByStatus[status],
431
+ message: error.message,
432
+ requestId,
333
433
  });
334
434
  return;
335
435
  }
336
436
 
337
- request.reply.status = 500;
338
- request.reply.headers["content-type"] = "application/json";
339
- request.reply.body = JSON.stringify({
437
+ reply.status = 500;
438
+ headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
439
+ reply.body = JSON.stringify({
340
440
  status: 500,
341
- error: "InternalServerError",
342
- message: (error as Error).message,
343
- requestId: request.requestId,
441
+ error: ERROR_INTERNAL,
442
+ message: error.message,
443
+ requestId,
344
444
  });
345
445
  }
346
446
  }
347
447
  }
348
448
 
449
+ /**
450
+ * Validate incoming request against route schema.
451
+ */
349
452
  public validateRequest(
350
453
  route: { schema?: RequestConfigSchema },
351
454
  request: ServerRequestConfig,
352
455
  ) {
456
+ // Validate params (path parameters)
353
457
  if (route.schema?.params) {
354
458
  try {
355
459
  request.params = this.alepha.codec.validate(
@@ -361,24 +465,32 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
361
465
  }
362
466
  }
363
467
 
468
+ // Validate query parameters (?key=value&key2=value2)
364
469
  if (route.schema?.query) {
365
470
  try {
366
- // we parse one by one to use the TypeBox coercion (e.g., number from string)
471
+ // Use cached keys instead of for...in enumeration
472
+ const schemaQuery = route.schema.query;
473
+ const keys = this.getQuerySchemaKeys(schemaQuery);
367
474
  const query: Record<string, any> = {};
368
- for (const key in route.schema.query.properties) {
475
+
476
+ // Use indexed loop for better performance than for...of
477
+ for (let i = 0; i < keys.length; i++) {
478
+ const key = keys[i];
369
479
  if (request.query[key] != null) {
370
480
  query[key] = this.alepha.codec.decode(
371
- route.schema.query.properties[key],
481
+ schemaQuery.properties[key],
372
482
  request.query[key],
373
483
  );
374
484
  }
375
485
  }
486
+
376
487
  request.query = query;
377
488
  } catch (error) {
378
489
  throw new ValidationError("Invalid request query", error);
379
490
  }
380
491
  }
381
492
 
493
+ // Validate headers
382
494
  if (route.schema?.headers) {
383
495
  try {
384
496
  request.headers = this.alepha.codec.validate(
@@ -390,6 +502,7 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
390
502
  }
391
503
  }
392
504
 
505
+ // Validate body
393
506
  if (route.schema?.body) {
394
507
  if (t.schema.isString(route.schema.body)) {
395
508
  if (typeof request.body !== "string") {
@@ -408,3 +521,34 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
408
521
  }
409
522
  }
410
523
  }
524
+
525
+ // ---------------------------------------------------------------------------------------------------------------------
526
+
527
+ // Pre-allocated encode options for response serialization
528
+ const ENCODE_OPTIONS_STRING = Object.freeze({
529
+ as: "string" as const,
530
+ });
531
+
532
+ // HTTP Headers
533
+ const HEADER_CONTENT_TYPE = "content-type";
534
+ const HEADER_CONTENT_DISPOSITION = "content-disposition";
535
+ const HEADER_REQUEST_ID = "x-request-id";
536
+ const HEADER_CORRELATION_ID = "x-correlation-id";
537
+
538
+ // Content Types
539
+ const CONTENT_TYPE_JSON = "application/json";
540
+ const CONTENT_TYPE_TEXT = "text/plain";
541
+ const CONTENT_TYPE_HTML = "text/html; charset=UTF-8";
542
+ const CONTENT_TYPE_OCTET = "application/octet-stream";
543
+
544
+ // Timing Keys
545
+ const TIMING_VALIDATE = "validateRequest";
546
+ const TIMING_HANDLER = "runHandler";
547
+ const TIMING_SERIALIZE = "serializeResponse";
548
+
549
+ // Context Keys
550
+ const CTX_REQUEST = "request";
551
+
552
+ // Error Messages
553
+ const ERROR_INTERNAL = "InternalServerError";
554
+ const ERROR_NOT_FILE = "Invalid response body - not a file";
@@ -18,7 +18,7 @@ export class ServerTimingProvider {
18
18
  public readonly onRequest = $hook({
19
19
  priority: "first",
20
20
  on: "server:onRequest",
21
- handler: async ({ request }) => {
21
+ handler: ({ request }) => {
22
22
  if (this.options.disabled) {
23
23
  return;
24
24
  }
@@ -31,7 +31,7 @@ export class ServerTimingProvider {
31
31
  public readonly onResponse = $hook({
32
32
  priority: "last",
33
33
  on: "server:onResponse",
34
- handler: async ({ request }) => {
34
+ handler: ({ request }) => {
35
35
  if (this.options.disabled) {
36
36
  return;
37
37
  }
@@ -0,0 +1,7 @@
1
+ import { $atom, t } from "alepha";
2
+ import { apiLinksResponseSchema } from "../schemas/apiLinksResponseSchema.ts";
3
+
4
+ export const apiLinksAtom = $atom({
5
+ name: "alepha.server.request.apiLinks",
6
+ schema: t.optional(apiLinksResponseSchema),
7
+ });
@@ -1,4 +1,5 @@
1
1
  import { $module } from "alepha";
2
+ import { apiLinksAtom } from "./atoms/apiLinksAtom.ts";
2
3
  import { $client } from "./primitives/$client.ts";
3
4
  import { $remote } from "./primitives/$remote.ts";
4
5
  import { LinkProvider } from "./providers/LinkProvider.ts";
@@ -14,6 +15,7 @@ export * from "./schemas/apiLinksResponseSchema.ts";
14
15
 
15
16
  export const AlephaServerLinks = $module({
16
17
  name: "alepha.server.links",
18
+ atoms: [apiLinksAtom],
17
19
  primitives: [$remote, $client],
18
20
  services: [LinkProvider],
19
21
  });
@@ -1,6 +1,7 @@
1
- import "alepha/server/security";
1
+ import "alepha/security";
2
2
  import { $module } from "alepha";
3
3
  import { AlephaServer } from "alepha/server";
4
+ import { apiLinksAtom } from "./atoms/apiLinksAtom.ts";
4
5
  import { $client } from "./primitives/$client.ts";
5
6
  import { $remote } from "./primitives/$remote.ts";
6
7
  import { LinkProvider } from "./providers/LinkProvider.ts";
@@ -46,6 +47,7 @@ declare module "alepha" {
46
47
  */
47
48
  export const AlephaServerLinks = $module({
48
49
  name: "alepha.server.links",
50
+ atoms: [apiLinksAtom],
49
51
  primitives: [$remote, $client],
50
52
  services: [
51
53
  AlephaServer,
@@ -7,6 +7,7 @@ import {
7
7
  t,
8
8
  } from "alepha";
9
9
  import { $logger } from "alepha/logger";
10
+ import type { ServerRouteSecure } from "alepha/security";
10
11
  import {
11
12
  type ActionPrimitive,
12
13
  type ClientRequestEntry,
@@ -22,7 +23,6 @@ import {
22
23
  type TRequestBody,
23
24
  UnauthorizedError,
24
25
  } from "alepha/server";
25
- import type { ServerRouteSecure } from "alepha/server/security";
26
26
  import {
27
27
  type ApiLink,
28
28
  apiLinksResponseSchema,
@@ -1,4 +1,4 @@
1
- import "alepha/server/security";
1
+ import "alepha/security";
2
2
  import { $module } from "alepha";
3
3
  import { AlephaServer, type RequestConfigSchema } from "alepha/server";
4
4
  import { AlephaServerCache } from "alepha/server/cache";