alepha 0.15.0 → 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 (222) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +240 -240
  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 +185 -185
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -2
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +245 -245
  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 +74 -74
  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 +221 -221
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.d.ts +1632 -1631
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +26 -34
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/api/verifications/index.d.ts +132 -132
  25. package/dist/api/verifications/index.d.ts.map +1 -1
  26. package/dist/batch/index.d.ts +122 -122
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/bucket/index.d.ts +163 -163
  29. package/dist/bucket/index.d.ts.map +1 -1
  30. package/dist/cache/core/index.d.ts +46 -46
  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 +5933 -201
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +609 -169
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +296 -296
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +19 -19
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +268 -79
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +768 -694
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +268 -79
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +268 -79
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts +44 -44
  52. package/dist/datetime/index.d.ts.map +1 -1
  53. package/dist/email/index.d.ts +25 -25
  54. package/dist/email/index.d.ts.map +1 -1
  55. package/dist/fake/index.d.ts +5409 -5409
  56. package/dist/fake/index.d.ts.map +1 -1
  57. package/dist/fake/index.js +22 -22
  58. package/dist/fake/index.js.map +1 -1
  59. package/dist/file/index.d.ts +435 -435
  60. package/dist/file/index.d.ts.map +1 -1
  61. package/dist/lock/core/index.d.ts +208 -208
  62. package/dist/lock/core/index.d.ts.map +1 -1
  63. package/dist/lock/redis/index.d.ts.map +1 -1
  64. package/dist/logger/index.d.ts +24 -24
  65. package/dist/logger/index.d.ts.map +1 -1
  66. package/dist/logger/index.js +1 -5
  67. package/dist/logger/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts +216 -198
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +28 -4
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/orm/index.browser.js +9 -9
  73. package/dist/orm/index.browser.js.map +1 -1
  74. package/dist/orm/index.bun.js +83 -76
  75. package/dist/orm/index.bun.js.map +1 -1
  76. package/dist/orm/index.d.ts +961 -960
  77. package/dist/orm/index.d.ts.map +1 -1
  78. package/dist/orm/index.js +88 -81
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +244 -244
  81. package/dist/queue/core/index.d.ts.map +1 -1
  82. package/dist/queue/redis/index.d.ts.map +1 -1
  83. package/dist/redis/index.d.ts +105 -105
  84. package/dist/redis/index.d.ts.map +1 -1
  85. package/dist/retry/index.d.ts +69 -69
  86. package/dist/retry/index.d.ts.map +1 -1
  87. package/dist/router/index.d.ts +6 -6
  88. package/dist/router/index.d.ts.map +1 -1
  89. package/dist/scheduler/index.d.ts +108 -26
  90. package/dist/scheduler/index.d.ts.map +1 -1
  91. package/dist/scheduler/index.js +393 -1
  92. package/dist/scheduler/index.js.map +1 -1
  93. package/dist/security/index.d.ts +532 -209
  94. package/dist/security/index.d.ts.map +1 -1
  95. package/dist/security/index.js +1422 -11
  96. package/dist/security/index.js.map +1 -1
  97. package/dist/server/auth/index.d.ts +1296 -271
  98. package/dist/server/auth/index.d.ts.map +1 -1
  99. package/dist/server/auth/index.js +1249 -18
  100. package/dist/server/auth/index.js.map +1 -1
  101. package/dist/server/cache/index.d.ts +56 -56
  102. package/dist/server/cache/index.d.ts.map +1 -1
  103. package/dist/server/compress/index.d.ts +3 -3
  104. package/dist/server/compress/index.d.ts.map +1 -1
  105. package/dist/server/cookies/index.d.ts +6 -6
  106. package/dist/server/cookies/index.d.ts.map +1 -1
  107. package/dist/server/core/index.d.ts +196 -186
  108. package/dist/server/core/index.d.ts.map +1 -1
  109. package/dist/server/core/index.js +43 -27
  110. package/dist/server/core/index.js.map +1 -1
  111. package/dist/server/cors/index.d.ts +11 -11
  112. package/dist/server/cors/index.d.ts.map +1 -1
  113. package/dist/server/health/index.d.ts.map +1 -1
  114. package/dist/server/helmet/index.d.ts +2 -2
  115. package/dist/server/helmet/index.d.ts.map +1 -1
  116. package/dist/server/links/index.browser.js +9 -1
  117. package/dist/server/links/index.browser.js.map +1 -1
  118. package/dist/server/links/index.d.ts +83 -83
  119. package/dist/server/links/index.d.ts.map +1 -1
  120. package/dist/server/links/index.js +13 -5
  121. package/dist/server/links/index.js.map +1 -1
  122. package/dist/server/metrics/index.d.ts +514 -1
  123. package/dist/server/metrics/index.d.ts.map +1 -1
  124. package/dist/server/metrics/index.js +4462 -4
  125. package/dist/server/metrics/index.js.map +1 -1
  126. package/dist/server/multipart/index.d.ts +6 -6
  127. package/dist/server/multipart/index.d.ts.map +1 -1
  128. package/dist/server/proxy/index.d.ts +102 -102
  129. package/dist/server/proxy/index.d.ts.map +1 -1
  130. package/dist/server/rate-limit/index.d.ts +16 -16
  131. package/dist/server/rate-limit/index.d.ts.map +1 -1
  132. package/dist/server/static/index.d.ts +44 -44
  133. package/dist/server/static/index.d.ts.map +1 -1
  134. package/dist/server/swagger/index.d.ts +47 -47
  135. package/dist/server/swagger/index.d.ts.map +1 -1
  136. package/dist/sms/index.d.ts +11 -11
  137. package/dist/sms/index.d.ts.map +1 -1
  138. package/dist/sms/index.js +3 -3
  139. package/dist/sms/index.js.map +1 -1
  140. package/dist/thread/index.d.ts +71 -71
  141. package/dist/thread/index.d.ts.map +1 -1
  142. package/dist/thread/index.js +2 -2
  143. package/dist/thread/index.js.map +1 -1
  144. package/dist/topic/core/index.d.ts +318 -318
  145. package/dist/topic/core/index.d.ts.map +1 -1
  146. package/dist/topic/redis/index.d.ts +6 -6
  147. package/dist/topic/redis/index.d.ts.map +1 -1
  148. package/dist/vite/index.d.ts +2324 -1719
  149. package/dist/vite/index.d.ts.map +1 -1
  150. package/dist/vite/index.js +123 -475
  151. package/dist/vite/index.js.map +1 -1
  152. package/dist/websocket/index.browser.js +3 -3
  153. package/dist/websocket/index.browser.js.map +1 -1
  154. package/dist/websocket/index.d.ts +275 -275
  155. package/dist/websocket/index.d.ts.map +1 -1
  156. package/dist/websocket/index.js +3 -3
  157. package/dist/websocket/index.js.map +1 -1
  158. package/package.json +9 -9
  159. package/src/api/users/services/SessionService.ts +0 -10
  160. package/src/cli/apps/AlephaCli.ts +2 -2
  161. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
  162. package/src/cli/assets/apiHelloControllerTs.ts +2 -1
  163. package/src/cli/assets/biomeJson.ts +2 -1
  164. package/src/cli/assets/claudeMd.ts +9 -4
  165. package/src/cli/assets/dummySpecTs.ts +2 -1
  166. package/src/cli/assets/editorconfig.ts +2 -1
  167. package/src/cli/assets/mainBrowserTs.ts +2 -1
  168. package/src/cli/assets/mainCss.ts +24 -0
  169. package/src/cli/assets/tsconfigJson.ts +2 -1
  170. package/src/cli/assets/webAppRouterTs.ts +2 -1
  171. package/src/cli/assets/webHelloComponentTsx.ts +6 -2
  172. package/src/cli/atoms/appEntryOptions.ts +13 -0
  173. package/src/cli/atoms/buildOptions.ts +1 -1
  174. package/src/cli/atoms/changelogOptions.ts +1 -1
  175. package/src/cli/commands/build.ts +63 -47
  176. package/src/cli/commands/dev.ts +16 -33
  177. package/src/cli/commands/gen/env.ts +1 -1
  178. package/src/cli/commands/init.ts +17 -8
  179. package/src/cli/commands/lint.ts +1 -1
  180. package/src/cli/defineConfig.ts +9 -0
  181. package/src/cli/index.ts +2 -1
  182. package/src/cli/providers/AppEntryProvider.ts +131 -0
  183. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  184. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  185. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  186. package/src/cli/services/AlephaCliUtils.ts +33 -2
  187. package/src/cli/services/PackageManagerUtils.ts +13 -6
  188. package/src/cli/services/ProjectScaffolder.ts +72 -49
  189. package/src/core/Alepha.ts +2 -8
  190. package/src/core/primitives/$module.ts +12 -0
  191. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  192. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  193. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  194. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  195. package/src/mcp/errors/McpError.ts +30 -0
  196. package/src/mcp/index.ts +3 -0
  197. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  198. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  199. package/src/orm/services/Repository.ts +11 -0
  200. package/src/server/core/index.ts +1 -1
  201. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  202. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  203. package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
  204. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  205. package/src/server/core/providers/ServerProvider.ts +9 -12
  206. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  207. package/src/server/links/index.browser.ts +2 -0
  208. package/src/server/links/index.ts +2 -0
  209. package/src/vite/index.ts +3 -2
  210. package/src/vite/tasks/buildClient.ts +0 -1
  211. package/src/vite/tasks/buildServer.ts +68 -21
  212. package/src/vite/tasks/copyAssets.ts +5 -4
  213. package/src/vite/tasks/generateSitemap.ts +64 -23
  214. package/src/vite/tasks/index.ts +0 -2
  215. package/src/vite/tasks/prerenderPages.ts +49 -24
  216. package/src/cli/assets/indexHtml.ts +0 -15
  217. package/src/cli/commands/format.ts +0 -23
  218. package/src/vite/helpers/boot.ts +0 -117
  219. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  220. package/src/vite/tasks/devServer.ts +0 -71
  221. package/src/vite/tasks/runAlepha.ts +0 -270
  222. /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
@@ -107,15 +107,6 @@ export class PrettyFormatterProvider extends LogFormatterProvider {
107
107
  return "";
108
108
  }
109
109
 
110
- if (this.alepha.isViteDev()) {
111
- // Node.js - try to fix stack trace with Vite SSR helper
112
- // Actually, it works only because we have a global helper in viteAlephaDev.ts
113
- const gl = globalThis as Record<string, unknown>;
114
- if (typeof gl === "object" && typeof gl.ssrFixStacktrace === "function") {
115
- gl.ssrFixStacktrace(error);
116
- }
117
- }
118
-
119
110
  let str = error.stack ?? error.message;
120
111
 
121
112
  const anyError = error as any;
@@ -2,6 +2,16 @@ import { JsonRpcErrorCodes } from "../helpers/jsonrpc.ts";
2
2
 
3
3
  // ---------------------------------------------------------------------------------------------------------------------
4
4
 
5
+ /**
6
+ * MCP-specific error codes (application-specific codes in the -32000 to -32099 range).
7
+ */
8
+ export const McpErrorCodes = {
9
+ UNAUTHORIZED: -32001,
10
+ FORBIDDEN: -32003,
11
+ } as const;
12
+
13
+ // ---------------------------------------------------------------------------------------------------------------------
14
+
5
15
  export class McpError extends Error {
6
16
  name = "McpError";
7
17
  code: number;
@@ -70,3 +80,23 @@ export class McpInvalidParamsError extends McpError {
70
80
  super(message, JsonRpcErrorCodes.INVALID_PARAMS);
71
81
  }
72
82
  }
83
+
84
+ // ---------------------------------------------------------------------------------------------------------------------
85
+
86
+ export class McpUnauthorizedError extends McpError {
87
+ name = "McpUnauthorizedError";
88
+
89
+ constructor(message = "Unauthorized") {
90
+ super(message, McpErrorCodes.UNAUTHORIZED);
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------------------------------------------------
95
+
96
+ export class McpForbiddenError extends McpError {
97
+ name = "McpForbiddenError";
98
+
99
+ constructor(message = "Forbidden") {
100
+ super(message, McpErrorCodes.FORBIDDEN);
101
+ }
102
+ }
package/src/mcp/index.ts CHANGED
@@ -10,11 +10,14 @@ import { StdioMcpTransport } from "./transports/StdioMcpTransport.ts";
10
10
 
11
11
  export {
12
12
  McpError,
13
+ McpErrorCodes,
14
+ McpForbiddenError,
13
15
  McpInvalidParamsError,
14
16
  McpMethodNotFoundError,
15
17
  McpPromptNotFoundError,
16
18
  McpResourceNotFoundError,
17
19
  McpToolNotFoundError,
20
+ McpUnauthorizedError,
18
21
  } from "./errors/McpError.ts";
19
22
  export {
20
23
  createErrorResponse,
@@ -115,6 +115,11 @@ export class SseMcpTransport {
115
115
  secure: false,
116
116
  schema: {
117
117
  body: t.json(),
118
+ query: t.object({
119
+ token: t.optional(
120
+ t.text({ description: "API token for authentication" }),
121
+ ),
122
+ }),
118
123
  },
119
124
  handler: async (request) => {
120
125
  try {
@@ -131,12 +136,17 @@ export class SseMcpTransport {
131
136
  const rpcRequest = parseMessage(body);
132
137
 
133
138
  // Build context from request headers
134
- const context: McpContext = {
135
- headers: request.headers as Record<
136
- string,
137
- string | string[] | undefined
138
- >,
139
- };
139
+ const headers = { ...request.headers } as Record<
140
+ string,
141
+ string | string[] | undefined
142
+ >;
143
+
144
+ // Support token as query parameter (for clients that can't set headers)
145
+ if (request.query.token && !headers.authorization) {
146
+ headers.authorization = `Bearer ${request.query.token}`;
147
+ }
148
+
149
+ const context: McpContext = { headers };
140
150
 
141
151
  const response = await this.mcpServer.handleMessage(
142
152
  rpcRequest,
@@ -47,9 +47,7 @@ export class DrizzleKitProvider {
47
47
  await this.saveDevMigrations(provider, snapshot, entry);
48
48
  }
49
49
 
50
- this.log.info(
51
- `Db '${provider.name}' synchronization OK [${Date.now() - now}ms]`,
52
- );
50
+ this.log.info(`Sync with '${provider.name}' OK [${Date.now() - now}ms]`);
53
51
  }
54
52
 
55
53
  /**
@@ -81,8 +79,8 @@ export class DrizzleKitProvider {
81
79
  };
82
80
  }
83
81
 
84
- const prev = prevSnapshot ?? (await kit.generateDrizzleJson({}));
85
- const curr = await kit.generateDrizzleJson(models);
82
+ const prev = prevSnapshot ?? kit.generateDrizzleJson({});
83
+ const curr = kit.generateDrizzleJson(models);
86
84
  return {
87
85
  models,
88
86
  statements: await kit.generateMigration(prev, curr),
@@ -69,6 +69,17 @@ export abstract class Repository<T extends TObject> {
69
69
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
70
70
  protected readonly alepha = $inject(Alepha);
71
71
 
72
+ static of<T extends TObject>(
73
+ entity: EntityPrimitive<T>,
74
+ provider = DatabaseProvider,
75
+ ): new () => Repository<T> {
76
+ return class InlineRepository extends Repository<T> {
77
+ constructor() {
78
+ super(entity, provider);
79
+ }
80
+ };
81
+ }
82
+
72
83
  constructor(entity: EntityPrimitive<T>, provider = DatabaseProvider) {
73
84
  this.entity = entity;
74
85
  this.provider = this.alepha.inject(provider);
@@ -141,7 +141,7 @@ export const AlephaServer = $module({
141
141
  ServerRouterProvider,
142
142
  ],
143
143
  register: (alepha: Alepha) => {
144
- if (!alepha.isServerless() && !alepha.isViteDev()) {
144
+ if (!alepha.isServerless()) {
145
145
  if (alepha.isBun()) {
146
146
  alepha.with({
147
147
  optional: true,
@@ -107,7 +107,7 @@ export class BunHttpServerProvider extends ServerProvider {
107
107
  },
108
108
  });
109
109
 
110
- this.log.info(`Server listening on ${this.hostname}`);
110
+ this.log.info(`Server listening on ${this.hostname}/`);
111
111
  } catch (err) {
112
112
  this.log.error("Failed to start Bun server", err);
113
113
  throw err;
@@ -0,0 +1,125 @@
1
+ import { Alepha } from "alepha";
2
+ import { afterEach, describe, expect, test } from "vitest";
3
+ import { NodeHttpServerProvider } from "./NodeHttpServerProvider.ts";
4
+
5
+ describe("NodeHttpServerProvider", () => {
6
+ describe("graceful shutdown", () => {
7
+ let alepha: Alepha;
8
+ let server: NodeHttpServerProvider;
9
+
10
+ afterEach(async () => {
11
+ await alepha?.stop().catch(() => {});
12
+ });
13
+
14
+ test("dev mode: destroys connections immediately on close", async () => {
15
+ alepha = Alepha.create({ env: { NODE_ENV: "development" } });
16
+ alepha.with(NodeHttpServerProvider);
17
+
18
+ await alepha.start();
19
+ server = alepha.inject(NodeHttpServerProvider);
20
+
21
+ // Make a request to establish connection
22
+ await fetch(`${server.hostname}/`);
23
+
24
+ const startTime = Date.now();
25
+ await alepha.stop();
26
+ const elapsed = Date.now() - startTime;
27
+
28
+ // Should close instantly (under 100ms)
29
+ expect(elapsed).toBeLessThan(100);
30
+ expect(server.getConnectionsCount()).toBe(0);
31
+ });
32
+
33
+ test("production mode: waits for connections then closes", async () => {
34
+ alepha = Alepha.create({ env: { NODE_ENV: "production" } });
35
+ alepha.with(NodeHttpServerProvider);
36
+
37
+ await alepha.start();
38
+ server = alepha.inject(NodeHttpServerProvider);
39
+ server.options.shutdownTimeout = 500;
40
+
41
+ // Make a request to establish keep-alive connection
42
+ await fetch(`${server.hostname}/`);
43
+
44
+ const startTime = Date.now();
45
+ await alepha.stop();
46
+ const elapsed = Date.now() - startTime;
47
+
48
+ // In production, should not be instant (waits for graceful close or timeout)
49
+ // But should complete within timeout
50
+ expect(elapsed).toBeLessThan(server.options.shutdownTimeout + 100);
51
+ expect(server.getConnectionsCount()).toBe(0);
52
+ });
53
+
54
+ test("production mode: forces close after timeout", async () => {
55
+ alepha = Alepha.create({ env: { NODE_ENV: "production" } });
56
+ alepha.with(NodeHttpServerProvider);
57
+
58
+ await alepha.start();
59
+ server = alepha.inject(NodeHttpServerProvider);
60
+ server.options.shutdownTimeout = 50;
61
+
62
+ // Make a request to establish connection
63
+ await fetch(`${server.hostname}/`);
64
+
65
+ const startTime = Date.now();
66
+ await alepha.stop();
67
+ const elapsed = Date.now() - startTime;
68
+
69
+ // Should close around timeout
70
+ expect(elapsed).toBeLessThan(200);
71
+ expect(server.getConnectionsCount()).toBe(0);
72
+ });
73
+
74
+ test("connections are tracked and cleared", async () => {
75
+ alepha = Alepha.create({ env: { NODE_ENV: "development" } });
76
+ alepha.with(NodeHttpServerProvider);
77
+
78
+ await alepha.start();
79
+ server = alepha.inject(NodeHttpServerProvider);
80
+
81
+ // Make multiple requests
82
+ await fetch(`${server.hostname}/`);
83
+ await fetch(`${server.hostname}/`);
84
+
85
+ await alepha.stop();
86
+
87
+ // All connections cleared after stop
88
+ expect(server.getConnectionsCount()).toBe(0);
89
+ });
90
+
91
+ test("rejects new requests during shutdown", async () => {
92
+ alepha = Alepha.create({ env: { NODE_ENV: "production" } });
93
+ alepha.with(NodeHttpServerProvider);
94
+
95
+ await alepha.start();
96
+ server = alepha.inject(NodeHttpServerProvider);
97
+ server.options.shutdownTimeout = 500;
98
+
99
+ // Establish a connection to keep server busy
100
+ await fetch(`${server.hostname}/`);
101
+
102
+ // Start shutdown (don't await yet)
103
+ const stopPromise = alepha.stop();
104
+
105
+ // Give server.close() time to be called
106
+ await new Promise((r) => setTimeout(r, 10));
107
+
108
+ // New request should fail (server no longer accepting connections)
109
+ let error: Error | null = null;
110
+ try {
111
+ await fetch(`${server.hostname}/`, {
112
+ signal: AbortSignal.timeout(100),
113
+ });
114
+ } catch (e) {
115
+ error = e as Error;
116
+ }
117
+
118
+ // Should get a connection error (ECONNREFUSED or similar)
119
+ expect(error).not.toBeNull();
120
+
121
+ // Wait for shutdown to complete
122
+ await stopPromise;
123
+ });
124
+ });
125
+ });
@@ -4,6 +4,7 @@ import {
4
4
  type Server,
5
5
  type ServerResponse,
6
6
  } from "node:http";
7
+ import type { Socket } from "node:net";
7
8
  import { $env, $hook, $inject, Alepha, type Static, t } from "alepha";
8
9
  import { DateTimeProvider } from "alepha/datetime";
9
10
  import { $logger } from "alepha/logger";
@@ -34,6 +35,24 @@ export class NodeHttpServerProvider extends ServerProvider {
34
35
  protected readonly env = $env(envSchema);
35
36
  protected readonly router = $inject(ServerRouterProvider);
36
37
 
38
+ /** Track active connections for fast shutdown */
39
+ protected readonly connections = new Set<Socket>();
40
+
41
+ /** Get number of active connections */
42
+ public getConnectionsCount(): number {
43
+ return this.connections.size;
44
+ }
45
+
46
+ /** Server options */
47
+ public readonly options = {
48
+ /**
49
+ * Graceful shutdown timeout in ms.
50
+ * After this, remaining connections are forcefully closed.
51
+ * @default 30000
52
+ */
53
+ shutdownTimeout: 10000,
54
+ };
55
+
37
56
  public get hostname(): string {
38
57
  if (this.server.listening) {
39
58
  const address = this.server.address();
@@ -45,10 +64,7 @@ export class NodeHttpServerProvider extends ServerProvider {
45
64
  }
46
65
 
47
66
  // Pre-bound error handler to avoid function allocation per request
48
- protected readonly handleRequestError = (
49
- res: import("node:http").ServerResponse,
50
- err: Error,
51
- ) => {
67
+ protected readonly handleRequestError = (res: ServerResponse, err: Error) => {
52
68
  this.log.error("Error handling request", err);
53
69
  res.statusCode = 500;
54
70
  res.end("Internal Server Error");
@@ -83,25 +99,27 @@ export class NodeHttpServerProvider extends ServerProvider {
83
99
  protected createHttpServer(
84
100
  func: (req: IncomingMessage, res: ServerResponse) => void,
85
101
  ): Server {
86
- return createServer(
102
+ const server = createServer(
87
103
  {
88
104
  // nov 25 - keep connections alive for better performance, cuz we http/1.1 by default
89
105
  keepAlive: this.alepha.isProduction(),
90
106
  },
91
107
  func,
92
108
  );
109
+
110
+ // Track connections for fast shutdown
111
+ server.on("connection", (socket) => {
112
+ this.connections.add(socket);
113
+ socket.on("close", () => this.connections.delete(socket));
114
+ });
115
+
116
+ return server;
93
117
  }
94
118
 
95
119
  protected readonly stop = $hook({
96
120
  on: "stop",
97
121
  handler: async () => {
98
- if (this.alepha.isProduction()) {
99
- await this.close();
100
- return;
101
- }
102
-
103
- // do not await in development & test
104
- this.close().catch(() => {});
122
+ await this.close();
105
123
  },
106
124
  });
107
125
 
@@ -115,7 +133,7 @@ export class NodeHttpServerProvider extends ServerProvider {
115
133
 
116
134
  await new Promise<void>((resolve, reject) => {
117
135
  this.server?.listen(port, this.env.SERVER_HOST, () => {
118
- this.log.info(`Server listening on ${this.hostname}`);
136
+ this.log.info(`Server listening on ${this.hostname}/`);
119
137
  resolve();
120
138
  });
121
139
 
@@ -126,18 +144,49 @@ export class NodeHttpServerProvider extends ServerProvider {
126
144
  }
127
145
 
128
146
  protected async close() {
129
- const promise = new Promise<void>((resolve, reject) => {
130
- this.server?.close((err) => {
131
- if (err) {
132
- reject(err);
133
- } else {
134
- resolve();
135
- }
136
- });
147
+ // Dev/Test: instant shutdown (destroy connections immediately)
148
+ // Production: graceful shutdown (wait for requests to complete, then close)
149
+ if (!this.alepha.isProduction()) {
150
+ this.destroyAllConnections();
151
+ }
152
+
153
+ // Stop accepting new connections
154
+ const closePromise = new Promise<void>((resolve, reject) => {
155
+ this.server?.close((err) => (err ? reject(err) : resolve()));
137
156
  });
138
157
 
139
- await Promise.race([this.dateTimeProvider.wait(2000), promise]);
158
+ if (this.alepha.isProduction() && this.connections.size > 0) {
159
+ // In production, wait for connections with timeout
160
+ const timeout = this.options.shutdownTimeout;
161
+
162
+ // Set up timeout to force-close connections
163
+ const timeoutId = setTimeout(() => {
164
+ if (this.connections.size > 0) {
165
+ this.log.warn(
166
+ `Shutdown timeout (${timeout}ms) reached, forcing ${this.connections.size} connections to close`,
167
+ );
168
+ // Destroy sockets - this triggers 'close' events which eventually resolves closePromise
169
+ for (const socket of this.connections) {
170
+ socket.destroy();
171
+ }
172
+ }
173
+ }, timeout);
174
+
175
+ // Wait for server to fully close (all connections closed)
176
+ await closePromise;
177
+ clearTimeout(timeoutId);
178
+ this.connections.clear();
179
+ } else {
180
+ await closePromise;
181
+ }
140
182
 
141
183
  this.log.info("Server closed");
142
184
  }
185
+
186
+ protected destroyAllConnections() {
187
+ for (const socket of this.connections) {
188
+ socket.destroy();
189
+ }
190
+ this.connections.clear();
191
+ }
143
192
  }
@@ -9,7 +9,7 @@ export class ServerLoggerProvider {
9
9
  on: "server:onRequest",
10
10
  priority: "first",
11
11
  handler: ({ route, request }) => {
12
- if (route.silent) {
12
+ if (route.silent || request.metadata.vite) {
13
13
  return;
14
14
  }
15
15
 
@@ -44,7 +44,7 @@ export class ServerLoggerProvider {
44
44
  on: "server:onResponse",
45
45
  priority: "last",
46
46
  handler: ({ route, request, response }) => {
47
- if (route.silent) {
47
+ if (route.silent || request.metadata.vite) {
48
48
  return;
49
49
  }
50
50
 
@@ -140,14 +140,6 @@ export class ServerProvider {
140
140
  }
141
141
  }
142
142
 
143
- /**
144
- * Extract pathname from URL without creating URL object.
145
- */
146
- protected getPathname(rawUrl: string): string {
147
- const qIndex = rawUrl.indexOf("?");
148
- return qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
149
- }
150
-
151
143
  public get hostname(): string {
152
144
  if (this.alepha.isViteDev()) {
153
145
  return `http://localhost:${this.alepha.env.SERVER_PORT}`;
@@ -185,11 +177,11 @@ export class ServerProvider {
185
177
  const rawUrl = req.url!;
186
178
  const { route, params } = this.router.match(`/${req.method}${rawUrl}`);
187
179
 
188
- if (this.isViteNotFound(rawUrl, route, params)) {
189
- return;
190
- }
191
-
192
180
  if (!route) {
181
+ // Skip if response was already sent (e.g., by Vite middleware)
182
+ if (res.headersSent) {
183
+ return;
184
+ }
193
185
  // if no route is found, return basic 404
194
186
  // note: you should not use this in production, use a custom 404 page instead by adding a route /*
195
187
  res.writeHead(404, { "content-type": "text/plain" });
@@ -221,6 +213,11 @@ export class ServerProvider {
221
213
  .handler(request)
222
214
  .catch(this.handleInternalError);
223
215
 
216
+ // Skip if response was already sent (e.g., by Vite middleware)
217
+ if (res.headersSent) {
218
+ return;
219
+ }
220
+
224
221
  // empty body - just send status & headers
225
222
  if (!response.body) {
226
223
  res.writeHead(response.status, response.headers).end();
@@ -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
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,
package/src/vite/index.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import type { Alepha } from "alepha";
2
2
 
3
3
  // Helpers (for advanced use)
4
- export * from "./helpers/boot.ts";
5
4
  export * from "./helpers/createBufferedLogger.ts";
5
+ export * from "./helpers/importVite.ts";
6
+ export * from "./helpers/importViteReact.ts";
6
7
  // Plugins (public API)
7
- export * from "./plugins/viteAlephaDev.ts";
8
8
  export * from "./plugins/viteAlephaSsrPreload.ts";
9
9
  export * from "./plugins/viteCompress.ts";
10
10
  // Tasks (for CLI integration)
11
11
  export * from "./tasks/index.ts";
12
12
 
13
13
  declare global {
14
+ var __alepha: Alepha;
14
15
  var __cli_alepha: Alepha;
15
16
  }
@@ -103,7 +103,6 @@ export async function buildClient(opts: BuildClientOptions): Promise<void> {
103
103
  outDir: opts.dist,
104
104
  // Generate manifest for SSR module preloading
105
105
  manifest: true,
106
- ssrManifest: true,
107
106
  rollupOptions: {
108
107
  output: {
109
108
  entryFileNames: "entry.[hash].js",