bazis 0.99.5 → 0.99.6

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.
@@ -38,6 +38,8 @@ export interface HostedService {
38
38
  readonly planValidator?: HostedServicePlanValidator;
39
39
  /** Receives the application logger before start, when one is registered. */
40
40
  useDiagnostics?(diagnostics: HostedServiceDiagnostics): void;
41
+ /** Name in startup diagnostics, such as `infra:db`; the class name by default. */
42
+ readonly diagnosticName?: string;
41
43
  start(signal?: AbortSignal): void | Promise<void>;
42
44
  stop(): void | Promise<void>;
43
45
  /**
@@ -17,6 +17,8 @@ const DEFAULT_INFRA_PHASE = -100;
17
17
  */
18
18
  export class InfraLifecycle<TClient> implements HostedService {
19
19
  public readonly phase: number;
20
+ /** The manifest key, as in `/health`: `infra:db`. */
21
+ public readonly diagnosticName: string;
20
22
  private client?: TClient;
21
23
  private created = false;
22
24
  private disposed = false;
@@ -33,6 +35,7 @@ export class InfraLifecycle<TClient> implements HostedService {
33
35
  this.client = client;
34
36
  this.created = client !== undefined;
35
37
  this.phase = connector.phase ?? DEFAULT_INFRA_PHASE;
38
+ this.diagnosticName = `infra:${name}`;
36
39
  bindConnectorLifecycle(connector, this);
37
40
  }
38
41
 
@@ -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.99.5" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.99.6" }, 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");
@@ -134,8 +134,12 @@ export class OpenSearchClient {
134
134
  }
135
135
  }
136
136
 
137
- /** OpenSearch client token for injection into application services. */
138
- export const OPENSEARCH: InjectionToken<OpenSearchClient> = createToken<OpenSearchClient>("OpenSearch");
137
+ /**
138
+ * OpenSearch client of the `openSearchConnect()` connector, injected by type:
139
+ * `constructor(private readonly search: IOpenSearch)`.
140
+ */
141
+ export interface IOpenSearch extends OpenSearchClient {}
142
+ export const IOpenSearch = createToken<IOpenSearch>("IOpenSearch") as InjectionToken<OpenSearchClient>;
139
143
 
140
144
  /**
141
145
  * Config interface required by the OpenSearch connector. The search subsystem
@@ -278,7 +282,7 @@ async function readBoundedText(response: Response, maxBytes: number, label: stri
278
282
  * });
279
283
  * @Infra({ search: openSearchConnect(searchConfig) })
280
284
  * export class AppInfra {}
281
- * // injection by token: scoped(ISearchService, SearchService, [OPENSEARCH] as const)
285
+ * // by type: class SearchService { constructor(private readonly search: IOpenSearch) {} }
282
286
  * ```
283
287
  */
284
288
  export function openSearchConnect<T extends OpenSearchConfigShape>(
@@ -286,7 +290,7 @@ export function openSearchConnect<T extends OpenSearchConfigShape>(
286
290
  options: OpenSearchConnectorOptions = {},
287
291
  ): InfraConnector<OpenSearchClient> {
288
292
  return {
289
- token: options.token ?? OPENSEARCH,
293
+ token: options.token ?? IOpenSearch,
290
294
  config,
291
295
  create(configs) {
292
296
  return new OpenSearchClient(buildOpenSearchOptions(config, options, configs));
@@ -4,8 +4,13 @@ import type { AppConfig, ConfigRegistry, Secret } from "../../kernel";
4
4
  import { reader, requireValue } from "../connectorConfig";
5
5
  import { errorMessage, InfraError, type InfraConnector } from "../InfraConnector";
6
6
 
7
- /** PostgreSQL client (native Bun `SQL`, no external dependencies). */
8
- export const POSTGRES: InjectionToken<SQL> = createToken<SQL>("Postgres");
7
+ /**
8
+ * PostgreSQL client (native Bun `SQL`, no external dependencies) of the
9
+ * `postgres()` connector, injected by type:
10
+ * `constructor(private readonly sql: IPostgres)`.
11
+ */
12
+ export interface IPostgres extends SQL {}
13
+ export const IPostgres = createToken<IPostgres>("IPostgres") as InjectionToken<SQL>;
9
14
 
10
15
  /**
11
16
  * Config interface required by the PostgreSQL connector. The database subsystem
@@ -69,7 +74,7 @@ export function postgresConnectionOptions(config: AppConfig<PostgresConfigShape>
69
74
  * });
70
75
  * @Infra({ db: postgres(dbConfig) })
71
76
  * export class AppInfra {}
72
- * // injection by token: scoped(IReportsService, ReportsService, [POSTGRES] as const)
77
+ * // by type: class ReportsService { constructor(private readonly sql: IPostgres) {} }
73
78
  * ```
74
79
  */
75
80
  export function postgres<T extends PostgresConfigShape>(config: AppConfig<T>, options: PostgresConnectorOptions = {}): InfraConnector<SQL> {
@@ -78,7 +83,7 @@ export function postgres<T extends PostgresConfigShape>(config: AppConfig<T>, op
78
83
  throw new InfraError("postgres shutdownTimeoutMs must be an integer 0..2147483647");
79
84
  }
80
85
  return {
81
- token: options.token ?? POSTGRES,
86
+ token: options.token ?? IPostgres,
82
87
  config,
83
88
  create(configs) {
84
89
  return new SQL(postgresConnectionOptions(config, configs));
@@ -11,9 +11,17 @@ import type { AppConfig, Secret } from "../../kernel";
11
11
  import { RedisDistributedCacheBackend, type RedisDistributedCacheTuning } from "../cache/RedisDistributedCacheBackend";
12
12
  import { reader, requireValue } from "../connectorConfig";
13
13
  import { errorMessage, InfraError, type InfraConnector } from "../InfraConnector";
14
+ import { redactSensitiveText } from "../../../library/redaction";
14
15
 
15
- /** Redis/Valkey client (native Bun `RedisClient`, no external dependencies). */
16
- export const REDIS: InjectionToken<RedisClient> = createToken<RedisClient>("Redis");
16
+ const DEFAULT_REDIS_CONNECT_TIMEOUT_MS = 5_000;
17
+
18
+ /**
19
+ * Redis/Valkey client (native Bun `RedisClient`, no external dependencies) of
20
+ * the `redisConnect()` connector, injected by type:
21
+ * `constructor(private readonly redis: IRedis)`.
22
+ */
23
+ export interface IRedis extends RedisClient {}
24
+ export const IRedis = createToken<IRedis>("IRedis") as InjectionToken<RedisClient>;
17
25
 
18
26
  /**
19
27
  * Config interface required by the Redis connector. The subsystem config
@@ -30,6 +38,12 @@ export type RedisConnectorCacheMode =
30
38
 
31
39
  export interface RedisConnectorOptions {
32
40
  readonly token?: InjectionToken<RedisClient>;
41
+ /**
42
+ * How long the start waits for the first connection, ms (default 5000).
43
+ * The Bun client keeps retrying a refused connection; without a bound an
44
+ * unreachable Redis held the whole start until its timeout.
45
+ */
46
+ readonly connectTimeoutMs?: number;
33
47
  /**
34
48
  * Publishes a distributed cache backend on top of the client: `{ cache: "distributed" }`.
35
49
  * For fine tuning use `{ cache: { mode: "distributed", keyPrefix, ... } }`.
@@ -81,7 +95,7 @@ function distributedCacheProviders(tuning: RedisDistributedCacheTuning, token: I
81
95
  * export const redisConfig = defineConfig("redis", { default: { url: "redis://localhost:6379" } });
82
96
  * @Infra({ cache: redisConnect(redisConfig, { cache: "distributed" }) })
83
97
  * export class AppInfra {}
84
- * // injection by token: scoped(ISessionsService, SessionsService, [REDIS] as const)
98
+ * // by type: class SessionsService { constructor(private readonly redis: IRedis) {} }
85
99
  * ```
86
100
  */
87
101
  export function redisConnect<T extends RedisConfigShape>(
@@ -89,7 +103,12 @@ export function redisConnect<T extends RedisConfigShape>(
89
103
  options: RedisConnectorOptions = {},
90
104
  ): InfraConnector<RedisClient> {
91
105
  const cache = options.cache;
92
- const token = options.token ?? REDIS;
106
+ const token = options.token ?? IRedis;
107
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_REDIS_CONNECT_TIMEOUT_MS;
108
+ if (!Number.isSafeInteger(connectTimeoutMs) || connectTimeoutMs <= 0) {
109
+ throw new InfraError("redisConnect connectTimeoutMs must be a positive integer.");
110
+ }
111
+ const urls = new WeakMap<RedisClient, string>();
93
112
  const cacheProviders =
94
113
  cache === undefined
95
114
  ? undefined
@@ -101,13 +120,28 @@ export function redisConnect<T extends RedisConfigShape>(
101
120
  providers: cacheProviders,
102
121
  exports: cacheProviders ? [DISTRIBUTED_CACHE_BACKEND, DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE, IDistributedCache] : undefined,
103
122
  create(configs) {
104
- return new RedisClient(redisUrl(reader(config, configs).get("url")));
123
+ const url = redisUrl(reader(config, configs).get("url"));
124
+ const client = new RedisClient(url);
125
+ urls.set(client, url);
126
+ return client;
105
127
  },
106
128
  async connect(client) {
129
+ // The address without a password: it names the server in the error.
130
+ const target = redactSensitiveText(urls.get(client) ?? "redis");
131
+ let timer: ReturnType<typeof setTimeout> | undefined;
132
+ const deadline = new Promise<never>((_resolve, reject) => {
133
+ timer = setTimeout(() => reject(new InfraError(`Infra connector "redis": failed to connect to ${target} within ${connectTimeoutMs} ms.`)), connectTimeoutMs);
134
+ });
107
135
  try {
108
- await client.connect();
136
+ await Promise.race([client.connect(), deadline]);
109
137
  } catch (error) {
110
- throw new InfraError(`Infra connector "redis": failed to connect — ${errorMessage(error)}`);
138
+ if (error instanceof InfraError) {
139
+ client.close();
140
+ throw error;
141
+ }
142
+ throw new InfraError(`Infra connector "redis": failed to connect to ${target} — ${errorMessage(error)}`);
143
+ } finally {
144
+ clearTimeout(timer);
111
145
  }
112
146
  },
113
147
  dispose(client) {
@@ -10,10 +10,10 @@ export { reader, requireValue, type ConfigReader } from "./connectorConfig";
10
10
  export { codexAppServerConnect, CODEX_APP_SERVER } from "./connectors/codex";
11
11
  export { CodexError, RU_CODEX_MESSAGES, type CodexErrorCode, CODEX_MODEL_ID, CODEX_REASONING_EFFORT, type CodexClient, type CodexConfigShape, type CodexLogin, type CodexLoginMethod, type CodexModel, type CodexStatus, type CodexRunInput, type CodexChatMessage, type CodexTool, type CodexToolCall, type CodexToolResult } from "./connectors/codex/contracts";
12
12
 
13
- export { postgres, POSTGRES, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
13
+ export { postgres, IPostgres, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
14
14
  export {
15
15
  redisConnect,
16
- REDIS,
16
+ IRedis,
17
17
  type RedisConfigShape,
18
18
  type RedisConnectorCacheMode,
19
19
  type RedisConnectorOptions,
@@ -26,7 +26,7 @@ export {
26
26
  } from "./cache";
27
27
  export {
28
28
  openSearchConnect,
29
- OPENSEARCH,
29
+ IOpenSearch,
30
30
  OpenSearchClient,
31
31
  type OpenSearchClientOptions,
32
32
  type OpenSearchConfigShape,
@@ -95,7 +95,7 @@ export class Kernel {
95
95
  let startupDisposal: Promise<void> | undefined;
96
96
  if (this.options.startupTimeoutMs > 0) {
97
97
  startupTimer = setTimeout(
98
- () => abortController.abort(new StartupTimeoutError(this.options.startupTimeoutMs)),
98
+ () => abortController.abort(new StartupTimeoutError(this.options.startupTimeoutMs, this.coordinator.startingServiceName)),
99
99
  this.options.startupTimeoutMs,
100
100
  );
101
101
  }
@@ -21,6 +21,13 @@ interface LifecycleResolver {
21
21
  * the {@link Kernel}, which bounds the whole graceful-stop sequence.
22
22
  */
23
23
  export class LifecycleCoordinator {
24
+ private startingName?: string;
25
+
26
+ /** The hosted service whose `start` is running now, for the startup timeout. */
27
+ public get startingServiceName(): string | undefined {
28
+ return this.startingName;
29
+ }
30
+
24
31
  private hooks: readonly LifecycleHook[] = [];
25
32
  /** Hosted services in actual start order (stop iterates in reverse). */
26
33
  private readonly started: HostedService[] = [];
@@ -81,6 +88,7 @@ export class LifecycleCoordinator {
81
88
  const group = phases[phaseIndex] as HostedService[];
82
89
  for (let index = 0; index < group.length; index += 1) {
83
90
  const service = group[index] as HostedService;
91
+ this.startingName = service.diagnosticName ?? service.constructor?.name;
84
92
  const starting = Promise.resolve().then(() => {
85
93
  signal?.throwIfAborted();
86
94
  return service.start(signal);
@@ -97,6 +105,7 @@ export class LifecycleCoordinator {
97
105
  throw error;
98
106
  }
99
107
  this.started.push(service);
108
+ this.startingName = undefined;
100
109
  }
101
110
  }
102
111
  this.hostedCount = this.started.length;
@@ -47,6 +47,11 @@ export class SupervisedHostedService implements HostedService {
47
47
  });
48
48
  }
49
49
 
50
+ public get diagnosticName(): string | undefined {
51
+ const service = this.inner ?? this.prepared;
52
+ return service === undefined ? undefined : service.diagnosticName ?? service.constructor.name;
53
+ }
54
+
50
55
  /** Passed on to every attempt's service before it starts. */
51
56
  public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
52
57
  this.diagnostics = diagnostics;
@@ -1,7 +1,10 @@
1
- import { KernelError } from "./KernelError";
2
-
3
- export class StartupTimeoutError extends KernelError {
4
- public constructor(public readonly timeoutMs: number) {
5
- super(`Application startup did not finish within ${timeoutMs}ms.`);
1
+ /**
2
+ * The start ran out of time. Not a configuration error: something it waited
3
+ * for did not answer, and `pending` names it when a hosted service was starting.
4
+ */
5
+ export class StartupTimeoutError extends Error {
6
+ public constructor(public readonly timeoutMs: number, public readonly pending?: string) {
7
+ super(`Application startup did not finish within ${timeoutMs}ms${pending === undefined ? "." : `; still starting: ${pending}.`}`);
8
+ this.name = "StartupTimeoutError";
6
9
  }
7
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.99.5",
3
+ "version": "0.99.6",
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",