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.
- package/README.md +43 -98
- package/dist/api/audits/index.d.ts +240 -240
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +2 -2
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +185 -185
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +2 -2
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -245
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.browser.js +4 -4
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +74 -74
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +4 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +221 -221
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.d.ts +1632 -1631
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +26 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +2 -2
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +5933 -201
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +609 -169
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +296 -296
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +19 -19
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +268 -79
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +768 -694
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +268 -79
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +268 -79
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/email/index.d.ts +25 -25
- package/dist/email/index.d.ts.map +1 -1
- package/dist/fake/index.d.ts +5409 -5409
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +22 -22
- package/dist/fake/index.js.map +1 -1
- package/dist/file/index.d.ts +435 -435
- package/dist/file/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +24 -24
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -5
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +216 -198
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +28 -4
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +9 -9
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +83 -76
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +961 -960
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +88 -81
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +244 -244
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.d.ts +105 -105
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +108 -26
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +393 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +532 -209
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1422 -11
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1296 -271
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1249 -18
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +56 -56
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/core/index.d.ts +196 -186
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +43 -27
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -11
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js +9 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +83 -83
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +13 -5
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4462 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -102
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +47 -47
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/sms/index.d.ts +11 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +3 -3
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -71
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +2 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +2324 -1719
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +123 -475
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +3 -3
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +275 -275
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +3 -3
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -9
- package/src/api/users/services/SessionService.ts +0 -10
- package/src/cli/apps/AlephaCli.ts +2 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
- package/src/cli/assets/apiHelloControllerTs.ts +2 -1
- package/src/cli/assets/biomeJson.ts +2 -1
- package/src/cli/assets/claudeMd.ts +9 -4
- package/src/cli/assets/dummySpecTs.ts +2 -1
- package/src/cli/assets/editorconfig.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +2 -1
- package/src/cli/assets/mainCss.ts +24 -0
- package/src/cli/assets/tsconfigJson.ts +2 -1
- package/src/cli/assets/webAppRouterTs.ts +2 -1
- package/src/cli/assets/webHelloComponentTsx.ts +6 -2
- package/src/cli/atoms/appEntryOptions.ts +13 -0
- package/src/cli/atoms/buildOptions.ts +1 -1
- package/src/cli/atoms/changelogOptions.ts +1 -1
- package/src/cli/commands/build.ts +63 -47
- package/src/cli/commands/dev.ts +16 -33
- package/src/cli/commands/gen/env.ts +1 -1
- package/src/cli/commands/init.ts +17 -8
- package/src/cli/commands/lint.ts +1 -1
- package/src/cli/defineConfig.ts +9 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/providers/AppEntryProvider.ts +131 -0
- package/src/cli/providers/ViteBuildProvider.ts +82 -0
- package/src/cli/providers/ViteDevServerProvider.ts +350 -0
- package/src/cli/providers/ViteTemplateProvider.ts +27 -0
- package/src/cli/services/AlephaCliUtils.ts +33 -2
- package/src/cli/services/PackageManagerUtils.ts +13 -6
- package/src/cli/services/ProjectScaffolder.ts +72 -49
- package/src/core/Alepha.ts +2 -8
- package/src/core/primitives/$module.ts +12 -0
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
- package/src/core/providers/SchemaValidator.spec.ts +236 -0
- package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
- package/src/mcp/errors/McpError.ts +30 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/transports/SseMcpTransport.ts +16 -6
- package/src/orm/providers/DrizzleKitProvider.ts +3 -5
- package/src/orm/services/Repository.ts +11 -0
- package/src/server/core/index.ts +1 -1
- package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
- package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
- package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
- package/src/server/core/providers/ServerProvider.ts +9 -12
- package/src/server/links/atoms/apiLinksAtom.ts +7 -0
- package/src/server/links/index.browser.ts +2 -0
- package/src/server/links/index.ts +2 -0
- package/src/vite/index.ts +3 -2
- package/src/vite/tasks/buildClient.ts +0 -1
- package/src/vite/tasks/buildServer.ts +68 -21
- package/src/vite/tasks/copyAssets.ts +5 -4
- package/src/vite/tasks/generateSitemap.ts +64 -23
- package/src/vite/tasks/index.ts +0 -2
- package/src/vite/tasks/prerenderPages.ts +49 -24
- package/src/cli/assets/indexHtml.ts +0 -15
- package/src/cli/commands/format.ts +0 -23
- package/src/vite/helpers/boot.ts +0 -117
- package/src/vite/plugins/viteAlephaDev.ts +0 -177
- package/src/vite/tasks/devServer.ts +0 -71
- package/src/vite/tasks/runAlepha.ts +0 -270
- /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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 ??
|
|
85
|
-
const curr =
|
|
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);
|
package/src/server/core/index.ts
CHANGED
|
@@ -141,7 +141,7 @@ export const AlephaServer = $module({
|
|
|
141
141
|
ServerRouterProvider,
|
|
142
142
|
],
|
|
143
143
|
register: (alepha: Alepha) => {
|
|
144
|
-
if (!alepha.isServerless()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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();
|
|
@@ -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",
|