alepha 0.14.3 → 0.14.4
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 +1 -1
- package/dist/api/audits/index.d.ts +338 -417
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +1 -80
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +156 -235
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.d.ts +170 -249
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +266 -345
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.d.ts +755 -834
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +125 -125
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/cli/index.d.ts +116 -20
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +212 -124
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -11
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2 -2
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +26 -4
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +16 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +26 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +26 -4
- package/dist/core/index.native.js.map +1 -1
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +12 -2
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.d.ts +37 -173
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +193 -422
- package/dist/orm/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +167 -167
- package/dist/server/cache/index.d.ts +12 -0
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +55 -2
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +6 -0
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +36 -1
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/core/index.browser.js +2 -2
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +10 -10
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +6 -3
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/security/index.d.ts +9 -9
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +4 -0
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +2 -3
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/vite/index.d.ts +101 -106
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +571 -508
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/apps/AlephaCli.ts +0 -2
- package/src/cli/atoms/buildOptions.ts +88 -0
- package/src/cli/commands/build.ts +32 -69
- package/src/cli/commands/db.ts +0 -4
- package/src/cli/commands/dev.ts +16 -4
- package/src/cli/commands/gen/env.ts +53 -0
- package/src/cli/commands/gen/openapi.ts +1 -1
- package/src/cli/commands/gen/resource.ts +15 -0
- package/src/cli/commands/gen.ts +7 -1
- package/src/cli/commands/init.ts +0 -1
- package/src/cli/commands/test.ts +0 -1
- package/src/cli/commands/verify.ts +1 -1
- package/src/cli/defineConfig.ts +49 -7
- package/src/cli/index.ts +0 -1
- package/src/cli/services/AlephaCliUtils.ts +36 -25
- package/src/command/helpers/Runner.spec.ts +2 -2
- package/src/command/helpers/Runner.ts +1 -1
- package/src/command/primitives/$command.ts +0 -6
- package/src/command/providers/CliProvider.ts +1 -3
- package/src/core/Alepha.ts +42 -0
- package/src/logger/index.ts +15 -3
- package/src/mcp/transports/StdioMcpTransport.ts +1 -1
- package/src/orm/index.ts +2 -8
- package/src/queue/core/providers/WorkerProvider.spec.ts +48 -32
- package/src/server/cache/providers/ServerCacheProvider.spec.ts +183 -0
- package/src/server/cache/providers/ServerCacheProvider.ts +94 -9
- package/src/server/compress/providers/ServerCompressProvider.ts +61 -2
- package/src/server/core/helpers/ServerReply.ts +2 -2
- package/src/server/core/providers/ServerProvider.ts +11 -1
- package/src/server/static/providers/ServerStaticProvider.ts +10 -0
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -8
- package/src/vite/helpers/importViteReact.ts +13 -0
- package/src/vite/index.ts +1 -21
- package/src/vite/plugins/viteAlephaDev.ts +16 -1
- package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
- package/src/vite/tasks/buildClient.ts +11 -0
- package/src/vite/tasks/buildServer.ts +47 -3
- package/src/vite/tasks/devServer.ts +69 -0
- package/src/vite/tasks/index.ts +2 -1
- package/src/cli/assets/viteConfigTs.ts +0 -14
- package/src/cli/commands/run.ts +0 -24
- package/src/vite/plugins/viteAlepha.ts +0 -37
- package/src/vite/plugins/viteAlephaBuild.ts +0 -281
package/src/core/Alepha.ts
CHANGED
|
@@ -165,6 +165,13 @@ export class Alepha {
|
|
|
165
165
|
...state.env,
|
|
166
166
|
...process.env,
|
|
167
167
|
};
|
|
168
|
+
|
|
169
|
+
// remove empty env variables
|
|
170
|
+
for (const key in state.env) {
|
|
171
|
+
if (state.env[key] === "") {
|
|
172
|
+
delete (state.env as any)[key];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
// force production mode when building with vite
|
|
@@ -908,6 +915,27 @@ export class Alepha {
|
|
|
908
915
|
return graph;
|
|
909
916
|
}
|
|
910
917
|
|
|
918
|
+
public dump(): AlephaDump {
|
|
919
|
+
const env: Record<string, AlephaDumpEnvVariable> = {};
|
|
920
|
+
for (const [schema] of this.cacheEnv.entries()) {
|
|
921
|
+
const ref = schema as any;
|
|
922
|
+
for (const [key, value] of Object.entries(ref.properties)) {
|
|
923
|
+
const prop = value as any;
|
|
924
|
+
env[key] = {
|
|
925
|
+
description: prop.description,
|
|
926
|
+
default: prop.default,
|
|
927
|
+
required: ref.required?.includes(key) ?? undefined,
|
|
928
|
+
enum: prop.enum ? ([...prop.enum] as Array<string>) : undefined,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
env,
|
|
935
|
+
providers: this.graph(),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
911
939
|
public services<T extends object>(base: Service<T>): Array<T> {
|
|
912
940
|
const list: Array<T> = [];
|
|
913
941
|
for (const [key, value] of this.registry.entries()) {
|
|
@@ -1012,6 +1040,20 @@ export interface Hook<T extends keyof Hooks = any> {
|
|
|
1012
1040
|
|
|
1013
1041
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
1014
1042
|
|
|
1043
|
+
export interface AlephaDump {
|
|
1044
|
+
env: Record<string, AlephaDumpEnvVariable>;
|
|
1045
|
+
providers: Record<string, { from: string[]; as?: string[]; module?: string }>;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export interface AlephaDumpEnvVariable {
|
|
1049
|
+
description: string;
|
|
1050
|
+
default?: string;
|
|
1051
|
+
required?: boolean;
|
|
1052
|
+
enum?: Array<string>;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1056
|
+
|
|
1015
1057
|
/**
|
|
1016
1058
|
* This is how we store services in the Alepha container.
|
|
1017
1059
|
*/
|
package/src/logger/index.ts
CHANGED
|
@@ -199,16 +199,28 @@ const envSchema = t.object({
|
|
|
199
199
|
* LOG_LEVEL=my.module.name:debug,info # Set debug level for my.module.name and info for all other modules
|
|
200
200
|
* LOG_LEVEL=alepha:trace, info # Set trace level for all alepha modules and info for all other modules
|
|
201
201
|
*/
|
|
202
|
-
LOG_LEVEL: t.optional(
|
|
202
|
+
LOG_LEVEL: t.optional(
|
|
203
|
+
t.text({
|
|
204
|
+
description: `Application log level on startup.
|
|
205
|
+
Levels are: trace, debug, info, warn, error, silent
|
|
206
|
+
Level can be set for a specific module:
|
|
207
|
+
"my.module.name:debug,info" -> Set debug level for my.module.name and info for all other modules
|
|
208
|
+
"alepha:trace,info" -> Set trace level for all alepha modules and info for all other modules`,
|
|
209
|
+
lowercase: true,
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
203
212
|
|
|
204
213
|
/**
|
|
205
214
|
* Built-in log formats.
|
|
206
215
|
* - "json" - JSON format, useful for structured logging and log aggregation. {@link JsonFormatterProvider}
|
|
207
216
|
* - "pretty" - Simple text format, human-readable, with colors. {@link PrettyFormatterProvider}
|
|
208
|
-
* - "raw" - Raw format, no formatting, just the message.
|
|
217
|
+
* - "raw" - Raw format, no formatting, just the message. {@link RawFormatterProvider}
|
|
209
218
|
*/
|
|
210
219
|
LOG_FORMAT: t.optional(
|
|
211
|
-
t.enum(["json", "pretty", "raw"], {
|
|
220
|
+
t.enum(["json", "pretty", "raw"], {
|
|
221
|
+
description: "Default log format for the application.",
|
|
222
|
+
lowercase: true,
|
|
223
|
+
}),
|
|
212
224
|
),
|
|
213
225
|
});
|
|
214
226
|
|
package/src/orm/index.ts
CHANGED
|
@@ -5,8 +5,6 @@ import { $entity } from "./primitives/$entity.ts";
|
|
|
5
5
|
import { $repository } from "./primitives/$repository.ts";
|
|
6
6
|
import { $sequence } from "./primitives/$sequence.ts";
|
|
7
7
|
import { DrizzleKitProvider } from "./providers/DrizzleKitProvider.ts";
|
|
8
|
-
import { BunPostgresProvider } from "./providers/drivers/BunPostgresProvider.ts";
|
|
9
|
-
import { BunSqliteProvider } from "./providers/drivers/BunSqliteProvider.ts";
|
|
10
8
|
import { CloudflareD1Provider } from "./providers/drivers/CloudflareD1Provider.ts";
|
|
11
9
|
import { DatabaseProvider } from "./providers/drivers/DatabaseProvider.ts";
|
|
12
10
|
import { NodePostgresProvider } from "./providers/drivers/NodePostgresProvider.ts";
|
|
@@ -116,8 +114,6 @@ export * from "./primitives/$sequence.ts";
|
|
|
116
114
|
export * from "./primitives/$transaction.ts";
|
|
117
115
|
export * from "./providers/DatabaseTypeProvider.ts";
|
|
118
116
|
export * from "./providers/DrizzleKitProvider.ts";
|
|
119
|
-
export * from "./providers/drivers/BunPostgresProvider.ts";
|
|
120
|
-
export * from "./providers/drivers/BunSqliteProvider.ts";
|
|
121
117
|
export * from "./providers/drivers/CloudflareD1Provider.ts";
|
|
122
118
|
export * from "./providers/drivers/DatabaseProvider.ts";
|
|
123
119
|
export * from "./providers/drivers/NodePostgresProvider.ts";
|
|
@@ -183,8 +179,6 @@ export const AlephaPostgres = $module({
|
|
|
183
179
|
NodePostgresProvider,
|
|
184
180
|
PglitePostgresProvider,
|
|
185
181
|
NodeSqliteProvider,
|
|
186
|
-
BunPostgresProvider,
|
|
187
|
-
BunSqliteProvider,
|
|
188
182
|
CloudflareD1Provider,
|
|
189
183
|
SqliteModelBuilder,
|
|
190
184
|
PostgresModelBuilder,
|
|
@@ -233,7 +227,7 @@ export const AlephaPostgres = $module({
|
|
|
233
227
|
alepha.with({
|
|
234
228
|
optional: true,
|
|
235
229
|
provide: DatabaseProvider,
|
|
236
|
-
use:
|
|
230
|
+
use: NodePostgresProvider,
|
|
237
231
|
});
|
|
238
232
|
return;
|
|
239
233
|
}
|
|
@@ -241,7 +235,7 @@ export const AlephaPostgres = $module({
|
|
|
241
235
|
alepha.with({
|
|
242
236
|
optional: true,
|
|
243
237
|
provide: DatabaseProvider,
|
|
244
|
-
use:
|
|
238
|
+
use: NodeSqliteProvider,
|
|
245
239
|
});
|
|
246
240
|
},
|
|
247
241
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Alepha, t } from "alepha";
|
|
2
|
+
import { $logger } from "alepha/logger";
|
|
2
3
|
import { describe, expect, test, vi } from "vitest";
|
|
3
4
|
import {
|
|
4
5
|
$consumer,
|
|
@@ -13,6 +14,16 @@ const payloadSchema = t.object({
|
|
|
13
14
|
count: t.integer(),
|
|
14
15
|
});
|
|
15
16
|
|
|
17
|
+
class TestWorkerProvider extends WorkerProvider {
|
|
18
|
+
public readonly log = $logger();
|
|
19
|
+
public workersRunning = 0;
|
|
20
|
+
public workerIntervals: Record<number, number> = {};
|
|
21
|
+
public abortController = new AbortController();
|
|
22
|
+
public waitForNextMessage(n: number): Promise<void> {
|
|
23
|
+
return super.waitForNextMessage(n);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
describe("WorkerProvider", () => {
|
|
17
28
|
const createTestApp = async (
|
|
18
29
|
options: {
|
|
@@ -29,6 +40,11 @@ describe("WorkerProvider", () => {
|
|
|
29
40
|
},
|
|
30
41
|
});
|
|
31
42
|
|
|
43
|
+
app.with({
|
|
44
|
+
provide: WorkerProvider,
|
|
45
|
+
use: TestWorkerProvider,
|
|
46
|
+
});
|
|
47
|
+
|
|
32
48
|
app.with({
|
|
33
49
|
provide: QueueProvider,
|
|
34
50
|
use: MemoryQueueProvider,
|
|
@@ -52,16 +68,16 @@ describe("WorkerProvider", () => {
|
|
|
52
68
|
const app = await createTestApp();
|
|
53
69
|
app.with(TestService);
|
|
54
70
|
|
|
55
|
-
const workerProvider = app.inject(
|
|
56
|
-
const logSpy = vi.spyOn(workerProvider
|
|
71
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
72
|
+
const logSpy = vi.spyOn(workerProvider.log, "debug");
|
|
57
73
|
|
|
58
74
|
await app.start();
|
|
59
75
|
|
|
60
76
|
expect(logSpy).toHaveBeenCalledWith("Starting worker n-0");
|
|
61
|
-
expect(workerProvider
|
|
77
|
+
expect(workerProvider.workersRunning).toBe(1);
|
|
62
78
|
|
|
63
79
|
await app.stop();
|
|
64
|
-
expect(workerProvider
|
|
80
|
+
expect(workerProvider.workersRunning).toBe(0);
|
|
65
81
|
});
|
|
66
82
|
|
|
67
83
|
test("should start multiple workers with concurrency", async () => {
|
|
@@ -76,32 +92,32 @@ describe("WorkerProvider", () => {
|
|
|
76
92
|
const app = await createTestApp({ workerConcurrency: 3 });
|
|
77
93
|
app.with(TestService);
|
|
78
94
|
|
|
79
|
-
const workerProvider = app.inject(
|
|
80
|
-
const logSpy = vi.spyOn(workerProvider
|
|
95
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
96
|
+
const logSpy = vi.spyOn(workerProvider.log, "debug");
|
|
81
97
|
|
|
82
98
|
await app.start();
|
|
83
99
|
|
|
84
100
|
expect(logSpy).toHaveBeenCalledWith("Starting worker n-0");
|
|
85
101
|
expect(logSpy).toHaveBeenCalledWith("Starting worker n-1");
|
|
86
102
|
expect(logSpy).toHaveBeenCalledWith("Starting worker n-2");
|
|
87
|
-
expect(workerProvider
|
|
103
|
+
expect(workerProvider.workersRunning).toBe(3);
|
|
88
104
|
|
|
89
105
|
await app.stop();
|
|
90
|
-
expect(workerProvider
|
|
106
|
+
expect(workerProvider.workersRunning).toBe(0);
|
|
91
107
|
});
|
|
92
108
|
|
|
93
109
|
test("should not start workers when no consumers", async () => {
|
|
94
110
|
const app = await createTestApp();
|
|
95
111
|
|
|
96
|
-
const workerProvider = app.inject(
|
|
97
|
-
const logSpy = vi.spyOn(workerProvider
|
|
112
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
113
|
+
const logSpy = vi.spyOn(workerProvider.log, "debug");
|
|
98
114
|
|
|
99
115
|
await app.start();
|
|
100
116
|
|
|
101
117
|
expect(logSpy).not.toHaveBeenCalledWith(
|
|
102
118
|
expect.stringMatching(/Starting worker/),
|
|
103
119
|
);
|
|
104
|
-
expect(workerProvider
|
|
120
|
+
expect(workerProvider.workersRunning).toBe(0);
|
|
105
121
|
|
|
106
122
|
await app.stop();
|
|
107
123
|
});
|
|
@@ -120,20 +136,20 @@ describe("WorkerProvider", () => {
|
|
|
120
136
|
const app = await createTestApp({ workerConcurrency: 2 });
|
|
121
137
|
app.with(TestService);
|
|
122
138
|
|
|
123
|
-
const workerProvider = app.inject(
|
|
124
|
-
const debugSpy = vi.spyOn(workerProvider
|
|
139
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
140
|
+
const debugSpy = vi.spyOn(workerProvider.log, "debug");
|
|
125
141
|
|
|
126
142
|
await app.start();
|
|
127
|
-
expect(workerProvider
|
|
143
|
+
expect(workerProvider.workersRunning).toBe(2);
|
|
128
144
|
|
|
129
145
|
// Simulate worker crash by manually decrementing counter
|
|
130
|
-
workerProvider
|
|
146
|
+
workerProvider.workersRunning = 1;
|
|
131
147
|
|
|
132
148
|
// Call wakeUp - should detect missing worker and restart it
|
|
133
149
|
workerProvider.wakeUp();
|
|
134
150
|
|
|
135
151
|
expect(debugSpy).toHaveBeenCalledWith("Waking up workers...");
|
|
136
|
-
expect(workerProvider
|
|
152
|
+
expect(workerProvider.workersRunning).toBe(2);
|
|
137
153
|
|
|
138
154
|
await app.stop();
|
|
139
155
|
});
|
|
@@ -150,16 +166,16 @@ describe("WorkerProvider", () => {
|
|
|
150
166
|
const app = await createTestApp();
|
|
151
167
|
app.with(TestService);
|
|
152
168
|
|
|
153
|
-
const workerProvider = app.inject(
|
|
169
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
154
170
|
|
|
155
171
|
await app.start();
|
|
156
172
|
|
|
157
|
-
const oldController = workerProvider
|
|
173
|
+
const oldController = workerProvider.abortController;
|
|
158
174
|
expect(oldController.signal.aborted).toBe(false);
|
|
159
175
|
|
|
160
176
|
workerProvider.wakeUp();
|
|
161
177
|
|
|
162
|
-
const newController = workerProvider
|
|
178
|
+
const newController = workerProvider.abortController;
|
|
163
179
|
expect(oldController.signal.aborted).toBe(true);
|
|
164
180
|
expect(newController.signal.aborted).toBe(false);
|
|
165
181
|
expect(newController).not.toBe(oldController);
|
|
@@ -217,8 +233,8 @@ describe("WorkerProvider", () => {
|
|
|
217
233
|
const app = await createTestApp();
|
|
218
234
|
app.with(TestService);
|
|
219
235
|
|
|
220
|
-
const workerProvider = app.inject(
|
|
221
|
-
const errorSpy = vi.spyOn(workerProvider
|
|
236
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
237
|
+
const errorSpy = vi.spyOn(workerProvider.log, "error");
|
|
222
238
|
|
|
223
239
|
await app.start();
|
|
224
240
|
|
|
@@ -230,7 +246,7 @@ describe("WorkerProvider", () => {
|
|
|
230
246
|
.toBeTruthy();
|
|
231
247
|
|
|
232
248
|
// Worker should still be running after processing error
|
|
233
|
-
expect(workerProvider
|
|
249
|
+
expect(workerProvider.workersRunning).toBe(1);
|
|
234
250
|
|
|
235
251
|
await app.stop();
|
|
236
252
|
});
|
|
@@ -282,9 +298,9 @@ describe("WorkerProvider", () => {
|
|
|
282
298
|
const app = await createTestApp();
|
|
283
299
|
app.with(TestService);
|
|
284
300
|
|
|
285
|
-
const workerProvider = app.inject(
|
|
301
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
286
302
|
const queueProvider = app.inject(QueueProvider);
|
|
287
|
-
const errorSpy = vi.spyOn(workerProvider
|
|
303
|
+
const errorSpy = vi.spyOn(workerProvider.log, "error");
|
|
288
304
|
|
|
289
305
|
await app.start();
|
|
290
306
|
|
|
@@ -301,7 +317,7 @@ describe("WorkerProvider", () => {
|
|
|
301
317
|
);
|
|
302
318
|
|
|
303
319
|
// Worker should still be running
|
|
304
|
-
expect(workerProvider
|
|
320
|
+
expect(workerProvider.workersRunning).toBe(1);
|
|
305
321
|
|
|
306
322
|
await app.stop();
|
|
307
323
|
});
|
|
@@ -318,9 +334,9 @@ describe("WorkerProvider", () => {
|
|
|
318
334
|
const app = await createTestApp();
|
|
319
335
|
app.with(TestService);
|
|
320
336
|
|
|
321
|
-
const workerProvider = app.inject(
|
|
337
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
322
338
|
const queueProvider = app.inject(QueueProvider);
|
|
323
|
-
const errorSpy = vi.spyOn(workerProvider
|
|
339
|
+
const errorSpy = vi.spyOn(workerProvider.log, "error");
|
|
324
340
|
|
|
325
341
|
await app.start();
|
|
326
342
|
|
|
@@ -342,7 +358,7 @@ describe("WorkerProvider", () => {
|
|
|
342
358
|
);
|
|
343
359
|
|
|
344
360
|
// Worker should still be running
|
|
345
|
-
expect(workerProvider
|
|
361
|
+
expect(workerProvider.workersRunning).toBe(1);
|
|
346
362
|
|
|
347
363
|
await app.stop();
|
|
348
364
|
});
|
|
@@ -359,16 +375,16 @@ describe("WorkerProvider", () => {
|
|
|
359
375
|
const app = await createTestApp({ workerInterval: 5000 });
|
|
360
376
|
app.with(TestService);
|
|
361
377
|
|
|
362
|
-
const workerProvider = app.inject(
|
|
363
|
-
const warnSpy = vi.spyOn(workerProvider
|
|
378
|
+
const workerProvider = app.inject(TestWorkerProvider);
|
|
379
|
+
const warnSpy = vi.spyOn(workerProvider.log, "warn");
|
|
364
380
|
|
|
365
381
|
await app.start();
|
|
366
382
|
|
|
367
383
|
// Abort the controller to simulate abort during wait
|
|
368
|
-
workerProvider
|
|
384
|
+
workerProvider.abortController.abort();
|
|
369
385
|
|
|
370
386
|
// This should detect the abort and return early
|
|
371
|
-
await workerProvider
|
|
387
|
+
await workerProvider.waitForNextMessage(0);
|
|
372
388
|
|
|
373
389
|
expect(warnSpy).toHaveBeenCalledWith("Worker n-0 aborted.");
|
|
374
390
|
|
|
@@ -865,6 +865,189 @@ describe("ServerCacheProvider", () => {
|
|
|
865
865
|
});
|
|
866
866
|
});
|
|
867
867
|
|
|
868
|
+
describe("Stream caching support", () => {
|
|
869
|
+
test("should cache ReadableStream responses via tee", async ({
|
|
870
|
+
expect,
|
|
871
|
+
}) => {
|
|
872
|
+
// Create a simple ReadableStream that emits chunks
|
|
873
|
+
const chunks = ["Hello", " ", "World"];
|
|
874
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
875
|
+
start(controller) {
|
|
876
|
+
const encoder = new TextEncoder();
|
|
877
|
+
for (const chunk of chunks) {
|
|
878
|
+
controller.enqueue(encoder.encode(chunk));
|
|
879
|
+
}
|
|
880
|
+
controller.close();
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Access the protected method via type assertion
|
|
885
|
+
const provider = cacheProvider as any;
|
|
886
|
+
const key = "test-stream-key";
|
|
887
|
+
|
|
888
|
+
// Collect the stream for cache
|
|
889
|
+
const hash = await provider.collectStreamForCache(
|
|
890
|
+
stream,
|
|
891
|
+
key,
|
|
892
|
+
200,
|
|
893
|
+
"text/html",
|
|
894
|
+
true,
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
expect(hash).toBeDefined();
|
|
898
|
+
expect(hash).toMatch(/^"[a-f0-9]+"$/); // ETag format
|
|
899
|
+
|
|
900
|
+
// Verify the cache contains the collected content
|
|
901
|
+
const cached = await provider.cache.get(key);
|
|
902
|
+
expect(cached).toBeDefined();
|
|
903
|
+
expect(cached.body).toBe("Hello World");
|
|
904
|
+
expect(cached.status).toBe(200);
|
|
905
|
+
expect(cached.contentType).toBe("text/html");
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("should tee stream so client and cache both receive data", async ({
|
|
909
|
+
expect,
|
|
910
|
+
}) => {
|
|
911
|
+
const encoder = new TextEncoder();
|
|
912
|
+
const chunks = ["<html>", "<body>", "Content", "</body>", "</html>"];
|
|
913
|
+
|
|
914
|
+
const originalStream = new ReadableStream<Uint8Array>({
|
|
915
|
+
start(controller) {
|
|
916
|
+
for (const chunk of chunks) {
|
|
917
|
+
controller.enqueue(encoder.encode(chunk));
|
|
918
|
+
}
|
|
919
|
+
controller.close();
|
|
920
|
+
},
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Tee the stream like ServerCacheProvider does
|
|
924
|
+
const [clientStream, cacheStream] = originalStream.tee();
|
|
925
|
+
|
|
926
|
+
// Read from client stream (simulates client receiving data)
|
|
927
|
+
const clientReader = clientStream.getReader();
|
|
928
|
+
const clientChunks: string[] = [];
|
|
929
|
+
const decoder = new TextDecoder();
|
|
930
|
+
|
|
931
|
+
while (true) {
|
|
932
|
+
const { done, value } = await clientReader.read();
|
|
933
|
+
if (done) break;
|
|
934
|
+
clientChunks.push(decoder.decode(value, { stream: true }));
|
|
935
|
+
}
|
|
936
|
+
clientChunks.push(decoder.decode()); // flush
|
|
937
|
+
|
|
938
|
+
// Read from cache stream (simulates cache collection)
|
|
939
|
+
const cacheReader = cacheStream.getReader();
|
|
940
|
+
const cacheChunks: string[] = [];
|
|
941
|
+
const cacheDecoder = new TextDecoder();
|
|
942
|
+
|
|
943
|
+
while (true) {
|
|
944
|
+
const { done, value } = await cacheReader.read();
|
|
945
|
+
if (done) break;
|
|
946
|
+
cacheChunks.push(cacheDecoder.decode(value, { stream: true }));
|
|
947
|
+
}
|
|
948
|
+
cacheChunks.push(cacheDecoder.decode()); // flush
|
|
949
|
+
|
|
950
|
+
// Both should receive the same data
|
|
951
|
+
const clientData = clientChunks.join("");
|
|
952
|
+
const cacheData = cacheChunks.join("");
|
|
953
|
+
|
|
954
|
+
expect(clientData).toBe("<html><body>Content</body></html>");
|
|
955
|
+
expect(cacheData).toBe("<html><body>Content</body></html>");
|
|
956
|
+
expect(clientData).toBe(cacheData);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test("should handle empty stream gracefully", async ({ expect }) => {
|
|
960
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
961
|
+
start(controller) {
|
|
962
|
+
controller.close();
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
const provider = cacheProvider as any;
|
|
967
|
+
const key = "empty-stream-key";
|
|
968
|
+
|
|
969
|
+
const hash = await provider.collectStreamForCache(
|
|
970
|
+
stream,
|
|
971
|
+
key,
|
|
972
|
+
200,
|
|
973
|
+
"text/html",
|
|
974
|
+
true,
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
expect(hash).toBeDefined();
|
|
978
|
+
|
|
979
|
+
const cached = await provider.cache.get(key);
|
|
980
|
+
expect(cached).toBeDefined();
|
|
981
|
+
expect(cached.body).toBe("");
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test("should handle large stream with multiple chunks", async ({
|
|
985
|
+
expect,
|
|
986
|
+
}) => {
|
|
987
|
+
const encoder = new TextEncoder();
|
|
988
|
+
const chunkCount = 100;
|
|
989
|
+
const chunkContent = "x".repeat(1000);
|
|
990
|
+
|
|
991
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
992
|
+
start(controller) {
|
|
993
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
994
|
+
controller.enqueue(encoder.encode(chunkContent));
|
|
995
|
+
}
|
|
996
|
+
controller.close();
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const provider = cacheProvider as any;
|
|
1001
|
+
const key = "large-stream-key";
|
|
1002
|
+
|
|
1003
|
+
const hash = await provider.collectStreamForCache(
|
|
1004
|
+
stream,
|
|
1005
|
+
key,
|
|
1006
|
+
200,
|
|
1007
|
+
"text/html",
|
|
1008
|
+
false,
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
// hash should be undefined when generateEtag is false
|
|
1012
|
+
expect(hash).toBeUndefined();
|
|
1013
|
+
|
|
1014
|
+
const cached = await provider.cache.get(key);
|
|
1015
|
+
expect(cached).toBeDefined();
|
|
1016
|
+
expect(cached.body.length).toBe(chunkCount * chunkContent.length);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test("should generate correct ETag for streamed content", async ({
|
|
1020
|
+
expect,
|
|
1021
|
+
}) => {
|
|
1022
|
+
const content = "Test content for ETag";
|
|
1023
|
+
const encoder = new TextEncoder();
|
|
1024
|
+
|
|
1025
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
1026
|
+
start(controller) {
|
|
1027
|
+
controller.enqueue(encoder.encode(content));
|
|
1028
|
+
controller.close();
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
const provider = cacheProvider as any;
|
|
1033
|
+
|
|
1034
|
+
// Generate ETag from stream
|
|
1035
|
+
const streamHash = await provider.collectStreamForCache(
|
|
1036
|
+
stream,
|
|
1037
|
+
"etag-test-key",
|
|
1038
|
+
200,
|
|
1039
|
+
"text/html",
|
|
1040
|
+
true,
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
// Generate ETag from string directly
|
|
1044
|
+
const stringHash = cacheProvider.generateETag(content);
|
|
1045
|
+
|
|
1046
|
+
// Both should produce the same ETag
|
|
1047
|
+
expect(streamHash).toBe(stringHash);
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
868
1051
|
describe("Error response caching", () => {
|
|
869
1052
|
test("should NOT cache 500 error responses", async ({ expect }) => {
|
|
870
1053
|
const response1 = await app.errorAction.fetch();
|
|
@@ -261,24 +261,55 @@ export class ServerCacheProvider {
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
// Only process string responses (text, html, json, etc.)
|
|
265
|
-
// Buffer is not supported by alepha/cache for now
|
|
266
|
-
if (typeof response.body !== "string") {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
264
|
// Don't cache error responses (status >= 400)
|
|
271
265
|
if (response.status && response.status >= 400) {
|
|
272
266
|
return;
|
|
273
267
|
}
|
|
274
268
|
|
|
269
|
+
// Initialize headers if not present
|
|
270
|
+
response.headers ??= {};
|
|
271
|
+
|
|
275
272
|
const key = this.createCacheKey(route, request);
|
|
273
|
+
|
|
274
|
+
// Handle ReadableStream responses (e.g., SSR streaming)
|
|
275
|
+
if (response.body instanceof ReadableStream && shouldStore) {
|
|
276
|
+
// Tee the stream: one for client, one for cache collection
|
|
277
|
+
const [clientStream, cacheStream] = (
|
|
278
|
+
response.body as ReadableStream<Uint8Array>
|
|
279
|
+
).tee();
|
|
280
|
+
|
|
281
|
+
// Replace response body with client stream (continues streaming to client)
|
|
282
|
+
response.body = clientStream as typeof response.body;
|
|
283
|
+
|
|
284
|
+
// Collect cache stream in background (non-blocking)
|
|
285
|
+
this.collectStreamForCache(
|
|
286
|
+
cacheStream,
|
|
287
|
+
key,
|
|
288
|
+
response.status,
|
|
289
|
+
response.headers?.["content-type"],
|
|
290
|
+
shouldUseEtag,
|
|
291
|
+
)
|
|
292
|
+
.then((hash) => {
|
|
293
|
+
if (shouldUseEtag && hash) {
|
|
294
|
+
// Note: headers already sent for streaming, etag only useful for future requests
|
|
295
|
+
this.log.trace("Stream cached with hash", { key, hash });
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
.catch((err) => {
|
|
299
|
+
this.log.warn("Failed to cache stream", { key, error: err });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Only process string responses (text, html, json, etc.)
|
|
306
|
+
if (typeof response.body !== "string") {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
276
310
|
const generatedEtag = this.generateETag(response.body);
|
|
277
311
|
const lastModified = this.time.toISOString();
|
|
278
312
|
|
|
279
|
-
// Initialize headers if not present
|
|
280
|
-
response.headers ??= {};
|
|
281
|
-
|
|
282
313
|
// Store response if storing is enabled
|
|
283
314
|
if (shouldStore) {
|
|
284
315
|
this.log.trace("Storing response", {
|
|
@@ -404,6 +435,60 @@ export class ServerCacheProvider {
|
|
|
404
435
|
|
|
405
436
|
return `${route.method}:${route.path.replaceAll(":", "")}:${params.join(",").replaceAll(":", "")}`;
|
|
406
437
|
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Collect a ReadableStream into a string and store it in the cache.
|
|
441
|
+
* This runs in the background while the original stream is sent to the client.
|
|
442
|
+
*
|
|
443
|
+
* @param stream - The stream to collect
|
|
444
|
+
* @param key - Cache key
|
|
445
|
+
* @param status - HTTP status code
|
|
446
|
+
* @param contentType - Content-Type header
|
|
447
|
+
* @param generateEtag - Whether to generate and return an ETag
|
|
448
|
+
* @returns The generated ETag hash, or undefined
|
|
449
|
+
*/
|
|
450
|
+
protected async collectStreamForCache(
|
|
451
|
+
stream: ReadableStream<Uint8Array>,
|
|
452
|
+
key: string,
|
|
453
|
+
status: number | undefined,
|
|
454
|
+
contentType: string | undefined,
|
|
455
|
+
generateEtag: boolean,
|
|
456
|
+
): Promise<string | undefined> {
|
|
457
|
+
const chunks: Uint8Array[] = [];
|
|
458
|
+
const reader = stream.getReader();
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
while (true) {
|
|
462
|
+
const { done, value } = await reader.read();
|
|
463
|
+
if (done) break;
|
|
464
|
+
chunks.push(value);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Combine chunks into a single string
|
|
468
|
+
const decoder = new TextDecoder();
|
|
469
|
+
const body =
|
|
470
|
+
chunks
|
|
471
|
+
.map((chunk) => decoder.decode(chunk, { stream: true }))
|
|
472
|
+
.join("") + decoder.decode(); // Flush remaining
|
|
473
|
+
|
|
474
|
+
const hash = this.generateETag(body);
|
|
475
|
+
const lastModified = this.time.toISOString();
|
|
476
|
+
|
|
477
|
+
this.log.trace("Storing streamed response", { key });
|
|
478
|
+
|
|
479
|
+
await this.cache.set(key, {
|
|
480
|
+
body,
|
|
481
|
+
status,
|
|
482
|
+
contentType,
|
|
483
|
+
lastModified,
|
|
484
|
+
hash,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return generateEtag ? hash : undefined;
|
|
488
|
+
} finally {
|
|
489
|
+
reader.releaseLock();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
407
492
|
}
|
|
408
493
|
|
|
409
494
|
export type ServerRouteCache =
|