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.
@@ -1,9 +1,10 @@
1
1
  import { DiError } from "./DiError";
2
2
 
3
3
  export class InvalidProviderError extends DiError {
4
- public constructor(tokenName: string) {
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
- super(`Cannot resolve scoped service "${tokenToDebugName(token)}" from root provider.`);
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
- const cyclePath = [...tokenPath, registration.token].map((token) => tokenToDebugName(token)).join(" -> ");
68
- issues.add(`Cycle detected: ${cyclePath}`);
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 Error {
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.7" }, capabilities: { experimentalApi: true },
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");
@@ -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
- console.error(`[bazis] ${redactSensitiveText(error.message)} Forcing exit.`);
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
- await this.invokeHook(() => (this.hooks[index] as LifecycleHook).onInit?.(), signal);
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)) issue(key, "conflict", "conflicting environment aliases");
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
- const details = Object.fromEntries(Object.entries(current).filter(([name]) => !SHOWN.has(name)));
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.7",
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",