bazis 0.100.7 → 0.100.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/di/errors/InvalidProviderError.ts +3 -2
- package/core/di/errors/ScopedServiceFromRootError.ts +2 -1
- package/core/di/internal/GraphValidator.ts +6 -2
- package/core/di/module/createContainer.ts +18 -1
- package/core/http/Errors/HttpError.ts +4 -7
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/kernel/Bazis.ts +9 -1
- package/core/kernel/LifecycleCoordinator.ts +10 -1
- package/core/kernel/config/defineConfig.ts +5 -1
- package/core/kernel/startupFailure.ts +4 -2
- package/library/http-client/HttpClient.ts +7 -0
- package/library/http-client/configKeys.ts +36 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DiError } from "./DiError";
|
|
2
2
|
|
|
3
3
|
export class InvalidProviderError extends DiError {
|
|
4
|
-
|
|
4
|
+
/** `message` replaces the default text when the caller knows more (the module and position). */
|
|
5
|
+
public constructor(tokenName: string, message?: string) {
|
|
5
6
|
super(
|
|
6
|
-
`Provider for "${tokenName}" has unknown shape: expected one of useClass/useFactory/useValue.`,
|
|
7
|
+
message ?? `Provider for "${tokenName}" has unknown shape: expected one of useClass/useFactory/useValue.`,
|
|
7
8
|
);
|
|
8
9
|
}
|
|
9
10
|
}
|
|
@@ -4,6 +4,7 @@ import { DiError } from "./DiError";
|
|
|
4
4
|
|
|
5
5
|
export class ScopedServiceFromRootError extends DiError {
|
|
6
6
|
public constructor(token: Token<unknown>) {
|
|
7
|
-
|
|
7
|
+
const name = tokenToDebugName(token);
|
|
8
|
+
super(`Cannot resolve scoped service "${name}" from root provider. A scoped service lives in a request or a scope: create one with provider.createScope(), resolve "${name}" from it and dispose the scope when done.`);
|
|
8
9
|
}
|
|
9
10
|
}
|
|
@@ -64,8 +64,12 @@ export class GraphValidator {
|
|
|
64
64
|
issues: Set<string>,
|
|
65
65
|
): void {
|
|
66
66
|
if (registrationPath.has(registration.id)) {
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// One cycle is reached from each of its members: report it once, as the cycle itself.
|
|
68
|
+
const start = tokenPath.indexOf(registration.token);
|
|
69
|
+
const members = (start >= 0 ? tokenPath.slice(start) : tokenPath).map((token) => tokenToDebugName(token));
|
|
70
|
+
const first = members.indexOf([...members].sort()[0] as string);
|
|
71
|
+
const canonical = [...members.slice(first), ...members.slice(0, first)];
|
|
72
|
+
issues.add(`Cycle detected: ${[...canonical, canonical[0]].join(" -> ")}`);
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -4,7 +4,7 @@ import { HOSTED_SERVICE, HOSTED_SERVICE_HEALTH, type HostedService } from "../ex
|
|
|
4
4
|
import { SERVICE_PROVIDER, SERVICE_PROVIDER_BY_TYPE } from "../extensions/service-provider-token";
|
|
5
5
|
import { isClassProvider, ProviderDefinition, type Provider } from "../provider";
|
|
6
6
|
import { getClassDeps } from "../internal/classDeps";
|
|
7
|
-
import { DiError, ModuleEncapsulationError } from "../errors";
|
|
7
|
+
import { DiError, InvalidProviderError, ModuleEncapsulationError } from "../errors";
|
|
8
8
|
import { OpenGenericRegistration } from "../internal/OpenGenericRegistration";
|
|
9
9
|
import { tokenToDebugName, type Class } from "../token";
|
|
10
10
|
import type { BuildServiceProviderOptions } from "../types";
|
|
@@ -133,6 +133,7 @@ export function createContainer(rootModule: BazisModuleRef, options?: CreateCont
|
|
|
133
133
|
const providers = moduleRef.providers;
|
|
134
134
|
if (providers) {
|
|
135
135
|
for (let index = 0; index < providers.length; index += 1) {
|
|
136
|
+
assertProviderDefinition(providers[index], record.name, index);
|
|
136
137
|
collection.add(providers[index] as ProviderDefinition);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
@@ -311,3 +312,19 @@ function normalizeLateGeneratedClassDeps(definition: ProviderDefinition): Provid
|
|
|
311
312
|
const normalizedProvider = { ...provider, deps: generatedDeps } as Provider<unknown>;
|
|
312
313
|
return new ProviderDefinition(normalizedProvider, definition.lifetime, definition.key);
|
|
313
314
|
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* A module lists registrations made by singleton/scoped/transient/…; a raw
|
|
318
|
+
* `{ provide: X }` object used to crash later with a bare TypeError.
|
|
319
|
+
*/
|
|
320
|
+
function assertProviderDefinition(value: unknown, moduleName: string, index: number): void {
|
|
321
|
+
const provider = (value as { provider?: unknown } | null | undefined)?.provider;
|
|
322
|
+
if (typeof provider === "object" && provider !== null && "provide" in provider) return;
|
|
323
|
+
const provide = (value as { provide?: unknown } | null | undefined)?.provide;
|
|
324
|
+
const name = typeof provide === "function" ? provide.name : typeof provide === "string" ? provide : String(provide ?? value);
|
|
325
|
+
throw new InvalidProviderError(
|
|
326
|
+
name,
|
|
327
|
+
`Module "${moduleName}": providers[${index}] is not a provider registration${provide === undefined ? "" : ` ({ provide: ${name} })`}. `
|
|
328
|
+
+ `Register it with singleton(${name}), scoped(${name}), transient(${name}), singletonValue(TOKEN, value) or singletonFactory(TOKEN, [deps], factory).`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KernelError } from "../../kernel";
|
|
1
2
|
import type { ModelValidationIssue } from "../Binding/modelValidator";
|
|
2
3
|
import { englishHttpMessage, type HttpMessageKey, type LocalizedMessage, type MessageParams } from "./messages";
|
|
3
4
|
|
|
@@ -111,11 +112,7 @@ export function frameworkBadRequest(key: HttpMessageKey, params?: MessageParams,
|
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
114
|
* Misconfigured routes/controllers (duplicate route, bad template, missing
|
|
114
|
-
* @Controller). Thrown at startup, never at request time — fail fast.
|
|
115
|
+
* @Controller). Thrown at startup, never at request time — fail fast. A
|
|
116
|
+
* KernelError: the start reports it as a configuration error of the code.
|
|
115
117
|
*/
|
|
116
|
-
export class HttpSetupError extends
|
|
117
|
-
constructor(message: string) {
|
|
118
|
-
super(message);
|
|
119
|
-
this.name = "HttpSetupError";
|
|
120
|
-
}
|
|
121
|
-
}
|
|
118
|
+
export class HttpSetupError extends KernelError {}
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.100.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.100.8" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
package/core/kernel/Bazis.ts
CHANGED
|
@@ -69,9 +69,17 @@ export class Bazis {
|
|
|
69
69
|
} catch (error) {
|
|
70
70
|
stopWaiting?.();
|
|
71
71
|
if (error instanceof ShutdownTimeoutError) {
|
|
72
|
+
// A failed start whose cleanup is stuck: report why the start failed
|
|
73
|
+
// first, otherwise only the cleanup timeout would be visible.
|
|
74
|
+
const startFailure = error.cause;
|
|
75
|
+
if (startFailure !== undefined) {
|
|
76
|
+
if (screen !== undefined) screen.failed(startFailure, kernel);
|
|
77
|
+
else console.error(`[bazis] application failed: ${formatStartupFailure(startFailure)}`);
|
|
78
|
+
}
|
|
72
79
|
// Graceful shutdown is stuck: hanging handles would keep the process
|
|
73
80
|
// alive forever, so this is the one place a hard exit is correct.
|
|
74
|
-
|
|
81
|
+
const text = startFailure === undefined ? error.message : `Cleanup after the failed start did not finish within ${error.timeoutMs}ms.`;
|
|
82
|
+
console.error(`[bazis] ${redactSensitiveText(text)} Forcing exit.`);
|
|
75
83
|
process.exit(1);
|
|
76
84
|
}
|
|
77
85
|
// Configuration problems surfacing at start (e.g. options fail-fast)
|
|
@@ -97,7 +97,16 @@ export class LifecycleCoordinator {
|
|
|
97
97
|
|
|
98
98
|
try {
|
|
99
99
|
for (let index = 0; index < this.hooks.length; index += 1) {
|
|
100
|
-
|
|
100
|
+
const hook = this.hooks[index] as LifecycleHook;
|
|
101
|
+
if (hook.onInit === undefined) continue;
|
|
102
|
+
// A hung onInit is named by the startup timeout, like a hosted service.
|
|
103
|
+
const name = `hook ${hook.constructor?.name ?? "LifecycleHook"}.onInit`;
|
|
104
|
+
this.pending.add(name);
|
|
105
|
+
try {
|
|
106
|
+
await this.invokeHook(() => hook.onInit?.(), signal);
|
|
107
|
+
} finally {
|
|
108
|
+
this.pending.delete(name);
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
const phases = groupByPhase(hosted);
|
|
@@ -184,7 +184,11 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
184
184
|
const priority = Math.max(...candidates.map(candidate => candidate.priority));
|
|
185
185
|
const winners = candidates.filter(candidate => candidate.priority === priority);
|
|
186
186
|
const selected = winners[0];
|
|
187
|
-
if (winners.some(candidate => candidate.raw !== selected?.raw))
|
|
187
|
+
if (winners.some(candidate => candidate.raw !== selected?.raw)) {
|
|
188
|
+
// Names only: the values may be secrets.
|
|
189
|
+
const variables = [...new Set(winners.map(candidate => candidate.name))];
|
|
190
|
+
issue(key, "conflict", `conflicting environment aliases: ${variables.join(" and ")} are set to different values; set only one`);
|
|
191
|
+
}
|
|
188
192
|
const raw = selected?.raw ?? (isSecret(cell) || isEnum(cell) ? cell.default : cell);
|
|
189
193
|
let value: ConfigValueType | undefined;
|
|
190
194
|
if (type === "secret") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { inspect } from "node:util";
|
|
2
2
|
import { redactSensitive, redactSensitiveText } from "../../library/redaction";
|
|
3
|
-
import { StartupFailuresError } from "./errors";
|
|
3
|
+
import { ShutdownTimeoutError, StartupFailuresError, StartupTimeoutError } from "./errors";
|
|
4
4
|
|
|
5
5
|
const MAX_CAUSES = 5;
|
|
6
6
|
/** Properties already printed in the header, the frames or the cause chain. */
|
|
@@ -32,7 +32,9 @@ export function formatStartupFailure(error: unknown): string {
|
|
|
32
32
|
// Services that failed together: the list above is the report; the
|
|
33
33
|
// aggregate's own frames are the kernel's, not the application's.
|
|
34
34
|
if (!(current instanceof StartupFailuresError)) lines.push(...(current.stack ?? "").split("\n").filter((line) => /^\s+at /.test(line)));
|
|
35
|
-
|
|
35
|
+
// Timeouts already say their limit and what was pending in the message.
|
|
36
|
+
const restated = current instanceof StartupTimeoutError || current instanceof ShutdownTimeoutError;
|
|
37
|
+
const details = restated ? {} : Object.fromEntries(Object.entries(current).filter(([name]) => !SHOWN.has(name)));
|
|
36
38
|
if (Object.keys(details).length > 0) lines.push(inspect(redactSensitive(details), INSPECT));
|
|
37
39
|
current = current.cause;
|
|
38
40
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpClientError, HttpErrorCode } from "./errors";
|
|
2
|
+
import { assertKnownOptions } from "./configKeys";
|
|
2
3
|
import { InspectableRedirectProtocol as redirectProtocol } from "./redirectProtocol";
|
|
3
4
|
import { InterceptorManager } from "./interceptors";
|
|
4
5
|
import { trackDownload } from "./progress";
|
|
@@ -85,6 +86,7 @@ export class HttpClient {
|
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
public constructor(defaults: RequestConfig = {}) {
|
|
89
|
+
assertKnownOptions(defaults, "the client defaults");
|
|
88
90
|
this.defaults = defaults;
|
|
89
91
|
this.interceptors = {
|
|
90
92
|
request: new InterceptorManager<ResolvedRequestConfig>(),
|
|
@@ -106,6 +108,11 @@ export class HttpClient {
|
|
|
106
108
|
public request<T = unknown>(configOrUrl: RequestConfig | string, maybeConfig: RequestConfig = {}): Promise<HttpResponse<T>> {
|
|
107
109
|
const config: RequestConfig =
|
|
108
110
|
typeof configOrUrl === "string" ? { ...maybeConfig, url: configOrUrl } : configOrUrl;
|
|
111
|
+
try {
|
|
112
|
+
assertKnownOptions(config, "the request config");
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return Promise.reject(error);
|
|
115
|
+
}
|
|
109
116
|
const merged = this.mergeConfig(this.defaults, config);
|
|
110
117
|
|
|
111
118
|
// axios ordering: request interceptors run last-registered-first.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HttpClientError } from "./errors";
|
|
2
|
+
import type { RequestConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every option of {@link RequestConfig}, checked so a misspelled one is not
|
|
6
|
+
* silently ignored. A record, not a list: a new option fails typecheck until
|
|
7
|
+
* it is added here.
|
|
8
|
+
*/
|
|
9
|
+
const OPTIONS: Readonly<Record<keyof RequestConfig, true>> = {
|
|
10
|
+
url: true, method: true, baseUrl: true, headers: true, params: true, paramsSerializer: true, data: true,
|
|
11
|
+
transformRequest: true, transformResponse: true, onDownloadProgress: true, timeoutMs: true, maxResponseBytes: true,
|
|
12
|
+
signal: true, responseType: true, withCredentials: true, credentials: true, auth: true,
|
|
13
|
+
allowCrossOriginCredentials: true, validateStatus: true, retry: true, propagateCorrelation: true,
|
|
14
|
+
correlationHeaders: true, fetch: true, inspectableRedirects: true, redirect: true, maxRedirects: true, cache: true,
|
|
15
|
+
mode: true, keepalive: true, meta: true,
|
|
16
|
+
};
|
|
17
|
+
const KNOWN: ReadonlySet<string> = new Set(Object.keys(OPTIONS));
|
|
18
|
+
|
|
19
|
+
/** axios names with a different spelling here. */
|
|
20
|
+
const AXIOS_NAMES: Readonly<Record<string, keyof RequestConfig>> = {
|
|
21
|
+
timeout: "timeoutMs",
|
|
22
|
+
maxContentLength: "maxResponseBytes",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rejects an option the client does not know. Without it `baseURL` (the axios
|
|
27
|
+
* spelling) was dropped, and every call failed with ERR_NETWORK
|
|
28
|
+
* "fetch() URL is invalid".
|
|
29
|
+
*/
|
|
30
|
+
export function assertKnownOptions(config: object, where: string): void {
|
|
31
|
+
for (const key of Object.keys(config)) {
|
|
32
|
+
if (KNOWN.has(key)) continue;
|
|
33
|
+
const suggestion = AXIOS_NAMES[key] ?? [...KNOWN].find((known) => known.toLowerCase() === key.toLowerCase());
|
|
34
|
+
throw new HttpClientError(`Unknown HttpClient option "${key}" in ${where}${suggestion === undefined ? "" : `; did you mean "${suggestion}"?`}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.8",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|