bazis 0.98.32 → 0.98.33

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,4 +1,4 @@
1
- import type { HostedService, HostedServiceDiagnostics } from "../di";
1
+ import type { HostedService, HostedServiceDiagnostics, HostedServiceHealth } from "../di";
2
2
  import { backgroundOptionsOf } from "./decorator";
3
3
  import { delay } from "./delay";
4
4
  import { redactSensitive } from "../../library/redaction";
@@ -56,6 +56,8 @@ export abstract class BackgroundService implements HostedService {
56
56
 
57
57
  /** The application logger, passed by the kernel; console until then. */
58
58
  private diagnostics?: HostedServiceDiagnostics;
59
+ /** Set when the service stopped for good after a crash: `/health` reports it. */
60
+ private terminated?: string;
59
61
 
60
62
  public constructor(options?: BackgroundServiceOptions) {
61
63
  // Explicit `super(...)` options win over `@Background({...})` metadata.
@@ -68,6 +70,15 @@ export abstract class BackgroundService implements HostedService {
68
70
  /** The background workload. Must observe `signal` for cooperative shutdown. */
69
71
  protected abstract execute(signal: AbortSignal): void | Promise<void>;
70
72
 
73
+ /**
74
+ * Unhealthy once the service stopped for good: it crashed without a restart
75
+ * policy, or used up its restarts. A pause before a restart, a tick failure
76
+ * of a periodic service and a normal end of `execute` stay healthy.
77
+ */
78
+ public checkHealth(): HostedServiceHealth {
79
+ return this.terminated === undefined ? { healthy: true } : { healthy: false, details: this.terminated };
80
+ }
81
+
71
82
  public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
72
83
  this.diagnostics = diagnostics;
73
84
  }
@@ -119,9 +130,11 @@ export abstract class BackgroundService implements HostedService {
119
130
  this.reportError(error, restarts);
120
131
  if (restarts >= maxRestarts) {
121
132
  // Without this line the service would just go quiet.
122
- report(this.diagnostics, "error", this.constructor.name, maxRestarts === 0
133
+ const stopped = maxRestarts === 0
123
134
  ? "stopped after a crash and will not run again (no restart policy)"
124
- : `stopped after ${maxRestarts} restart${maxRestarts === 1 ? "" : "s"} and will not run again`, { restarts });
135
+ : `stopped after ${maxRestarts} restart${maxRestarts === 1 ? "" : "s"} and will not run again`;
136
+ report(this.diagnostics, "error", this.constructor.name, stopped, { restarts });
137
+ this.terminated = `${stopped}: ${error instanceof Error ? error.message : String(error)}`;
125
138
  return;
126
139
  }
127
140
  restarts += 1;
@@ -26,7 +26,8 @@ interface BackgroundMetadataCarrier {
26
26
  * ```
27
27
  *
28
28
  * Options passed explicitly to `super(...)` take precedence over the decorator.
29
- * Registration stays explicit — list the class in `backgroundModule({ services })`.
29
+ * Registration stays explicit — list the class in `background` of its module:
30
+ * `@Module({ background: [Heartbeat] })`.
30
31
  */
31
32
  export function Background(options: BackgroundDecoratorOptions = {}) {
32
33
  return (_value: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext): void => {
@@ -10,6 +10,5 @@ export {
10
10
  type BackgroundServiceOptions,
11
11
  type PeriodicBackgroundServiceOptions,
12
12
  } from "./BackgroundService";
13
- export { backgroundModule, type BackgroundModuleConfig } from "./backgroundModule";
14
13
  export { Background, backgroundOptionsOf, type BackgroundDecoratorOptions } from "./decorator";
15
14
  export { delay } from "./delay";
@@ -22,7 +22,18 @@ export interface HostedServiceDiagnostics {
22
22
  warn(message: string, fields?: Readonly<Record<string, unknown>>): void;
23
23
  }
24
24
 
25
+ /** What a hosted service reports to `/health`. */
26
+ export interface HostedServiceHealth {
27
+ readonly healthy: boolean;
28
+ readonly details?: string;
29
+ }
30
+
25
31
  export interface HostedService {
32
+ /**
33
+ * The service's own state for `/health`. A `background` service of a module
34
+ * is reported as `background:<Class>`; without this method it is healthy.
35
+ */
36
+ checkHealth?(): HostedServiceHealth;
26
37
  /** Shared validators see real service identities, including inside supervised wrappers. */
27
38
  readonly planValidator?: HostedServicePlanValidator;
28
39
  /** Receives the application logger before start, when one is registered. */
@@ -39,6 +50,15 @@ export interface HostedService {
39
50
 
40
51
  export const HOSTED_SERVICE = createToken<HostedService>("IHostedService");
41
52
 
53
+ /** A health check contributed by a hosted service; the kernel reports it next to its own checks. */
54
+ export interface HostedServiceHealthCheck {
55
+ readonly name: string;
56
+ check(): HostedServiceHealth;
57
+ }
58
+
59
+ /** Enumerable: one entry per `background` service of the module graph. */
60
+ export const HOSTED_SERVICE_HEALTH = createToken<HostedServiceHealthCheck>("BazisHostedServiceHealth");
61
+
42
62
  interface HostedServiceResolver {
43
63
  resolveAll(token: typeof HOSTED_SERVICE): readonly HostedService[];
44
64
  }
@@ -1,6 +1,6 @@
1
1
  export { createInstance, type ActivatorDependency } from "./activator";
2
2
  export { Application, runApplication, type RunApplicationOptions } from "./application";
3
- export { addHostedService, HOSTED_SERVICE, startHostedServices, stopHostedServices, type HostedService, type HostedServiceDiagnostics, type HostedServicePlanValidator } from "./hosted-service";
3
+ export { addHostedService, HOSTED_SERVICE, HOSTED_SERVICE_HEALTH, startHostedServices, stopHostedServices, type HostedService, type HostedServiceDiagnostics, type HostedServiceHealth, type HostedServiceHealthCheck, type HostedServicePlanValidator } from "./hosted-service";
4
4
  export { SERVICE_PROVIDER } from "./service-provider-token";
5
5
  export {
6
6
  addOptions,
@@ -1,6 +1,6 @@
1
1
  import { DiContainer } from "../container";
2
2
  import { ServiceCollection } from "../ServiceCollection";
3
- import { HOSTED_SERVICE, type HostedService } from "../extensions/hosted-service";
3
+ import { HOSTED_SERVICE, HOSTED_SERVICE_HEALTH, type HostedService } from "../extensions/hosted-service";
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";
@@ -162,6 +162,13 @@ export function createContainer(rootModule: BazisModuleRef, options?: CreateCont
162
162
  collection.add(
163
163
  DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => resolver.resolve(ServiceClass))),
164
164
  );
165
+ // `/health` sees a service that stopped for good, not only the process.
166
+ collection.add(
167
+ DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE_HEALTH, [], (resolver) => {
168
+ const service = resolver.resolve(ServiceClass);
169
+ return { name: `background:${ServiceClass.name}`, check: () => service.checkHealth?.() ?? { healthy: true } };
170
+ })),
171
+ );
165
172
  }
166
173
  }
167
174
 
@@ -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.98.32" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.98.33" }, 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");
@@ -1,4 +1,4 @@
1
- import type { ServiceResolver } from "../../di";
1
+ import { HOSTED_SERVICE_HEALTH, type ServiceResolver } from "../../di";
2
2
  import { redactSensitiveText } from "../../../library/redaction";
3
3
  import { awaitAbortable } from "../internal/awaitAbortable";
4
4
  import { KernelError } from "../errors";
@@ -21,7 +21,8 @@ export class HealthService {
21
21
  const timeoutMs = bounded(options.timeoutMs ?? 5000, "timeoutMs", 2147483647);
22
22
  const checkTimeoutMs = bounded(options.checkTimeoutMs ?? 1000, "checkTimeoutMs", 2147483647);
23
23
  const concurrency = bounded(options.concurrency ?? 4, "concurrency", 1024);
24
- const checks = this.resolver.resolveAll(HEALTH_CHECK);
24
+ // Hosted services (background) report whether they still run.
25
+ const checks: readonly HealthCheck[] = [...this.resolver.resolveAll(HEALTH_CHECK), ...this.resolver.resolveAll(HOSTED_SERVICE_HEALTH)];
25
26
  const entries: HealthReportEntry[] = new Array(checks.length);
26
27
  const report = new AbortController();
27
28
  const timer = setTimeout(() => report.abort(new Error("Health report timed out.")), timeoutMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.98.32",
3
+ "version": "0.98.33",
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",
@@ -1,42 +0,0 @@
1
- import {
2
- DI,
3
- HOSTED_SERVICE,
4
- Module,
5
- singleton,
6
- type Class,
7
- type HostedService,
8
- type BazisModule,
9
- type ProviderDefinition,
10
- } from "../di";
11
-
12
- export interface BackgroundModuleConfig {
13
- /**
14
- * Background service classes (subclasses of `BackgroundService` /
15
- * `PeriodicBackgroundService`). Each is registered as a singleton (with
16
- * auto-resolved constructor dependencies) and exposed as a `HOSTED_SERVICE`,
17
- * so the kernel starts and gracefully stops it with the rest of the app.
18
- */
19
- readonly services: readonly Class<HostedService>[];
20
- }
21
-
22
- /**
23
- * Registers background services in DI and wires them into the kernel lifecycle.
24
- *
25
- * ```ts
26
- * @Module({ imports: [backgroundModule({ services: [SessionCleanup, Heartbeat] })] })
27
- * class AppModule {}
28
- * ```
29
- */
30
- export function backgroundModule(config: BackgroundModuleConfig): BazisModule {
31
- const providers: ProviderDefinition[] = config.services.flatMap((ServiceClass) => [
32
- singleton(ServiceClass),
33
- DI.singleton(
34
- DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => resolver.resolve(ServiceClass)),
35
- ),
36
- ]);
37
-
38
- @Module({ providers })
39
- class BackgroundModule {}
40
-
41
- return BackgroundModule;
42
- }