bazis 0.97.9 → 0.97.10

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 } from "../di";
1
+ import type { HostedService, HostedServiceDiagnostics } from "../di";
2
2
  import { backgroundOptionsOf } from "./decorator";
3
3
  import { delay } from "./delay";
4
4
  import { redactSensitive } from "../../library/redaction";
@@ -54,6 +54,9 @@ export abstract class BackgroundService implements HostedService {
54
54
  private controller?: AbortController;
55
55
  private runPromise?: Promise<void>;
56
56
 
57
+ /** The application logger, passed by the kernel; console until then. */
58
+ private diagnostics?: HostedServiceDiagnostics;
59
+
57
60
  public constructor(options?: BackgroundServiceOptions) {
58
61
  // Explicit `super(...)` options win over `@Background({...})` metadata.
59
62
  const resolved = { ...backgroundOptionsOf(this.constructor), ...options };
@@ -65,13 +68,22 @@ export abstract class BackgroundService implements HostedService {
65
68
  /** The background workload. Must observe `signal` for cooperative shutdown. */
66
69
  protected abstract execute(signal: AbortSignal): void | Promise<void>;
67
70
 
71
+ public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
72
+ this.diagnostics = diagnostics;
73
+ }
74
+
75
+ /** Reports a failure of this service: the application logger, or the console without one. */
76
+ protected reportFailure(message: string, error: unknown, fields: Readonly<Record<string, unknown>> = {}): void {
77
+ report(this.diagnostics, "error", this.constructor.name, message, { ...fields, error: redactSensitive(error) });
78
+ }
79
+
68
80
  public start(): void {
69
81
  if (this.controller) {
70
82
  return;
71
83
  }
72
84
  this.controller = new AbortController();
73
85
  this.runPromise = this.runSupervised(this.controller.signal)
74
- .catch((error: unknown) => logBackgroundError(this.constructor.name, error));
86
+ .catch((error: unknown) => this.reportFailure("supervisor failed", error));
75
87
  }
76
88
 
77
89
  public async stop(): Promise<void> {
@@ -83,7 +95,9 @@ export abstract class BackgroundService implements HostedService {
83
95
  this.controller = undefined;
84
96
  this.runPromise = undefined;
85
97
  if (pending) {
86
- await waitWithTimeout(pending, this.stopTimeoutMs, this.constructor.name);
98
+ await waitWithTimeout(pending, this.stopTimeoutMs, () => report(this.diagnostics, "warn", this.constructor.name,
99
+ `did not stop within ${this.stopTimeoutMs}ms: shutdown continues, but its unfinished work keeps the process alive until it ends`,
100
+ { stopTimeoutMs: this.stopTimeoutMs }));
87
101
  }
88
102
  }
89
103
 
@@ -104,6 +118,10 @@ export abstract class BackgroundService implements HostedService {
104
118
  }
105
119
  this.reportError(error, restarts);
106
120
  if (restarts >= maxRestarts) {
121
+ // Without this line the service would just go quiet.
122
+ report(this.diagnostics, "error", this.constructor.name, maxRestarts === 0
123
+ ? "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 });
107
125
  return;
108
126
  }
109
127
  restarts += 1;
@@ -117,9 +135,9 @@ export abstract class BackgroundService implements HostedService {
117
135
 
118
136
  private reportError(error: unknown, restarts: number): void {
119
137
  if (this.restart.onError) {
120
- observeDiagnostic(() => this.restart.onError!(error, restarts), this.constructor.name);
138
+ observeDiagnostic(() => this.restart.onError!(error, restarts), (failure) => this.reportFailure("onError callback failed", failure));
121
139
  } else {
122
- logBackgroundError(this.constructor.name, error);
140
+ this.reportFailure("crashed", error, { restarts });
123
141
  }
124
142
  }
125
143
  }
@@ -162,7 +180,7 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
162
180
 
163
181
  /** Hook for tick failures; default logs. The loop continues regardless. */
164
182
  protected onTickError(error: unknown): void {
165
- logBackgroundError(this.constructor.name, error);
183
+ this.reportFailure("tick failed", error);
166
184
  }
167
185
 
168
186
  protected override async execute(signal: AbortSignal): Promise<void> {
@@ -176,7 +194,7 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
176
194
  if (signal.aborted) {
177
195
  return;
178
196
  }
179
- observeDiagnostic(() => this.onTickError(error), this.constructor.name);
197
+ observeDiagnostic(() => this.onTickError(error), (failure) => this.reportFailure("onTickError failed", failure));
180
198
  }
181
199
  if (signal.aborted) {
182
200
  return;
@@ -187,20 +205,34 @@ export abstract class PeriodicBackgroundService extends BackgroundService {
187
205
  }
188
206
 
189
207
  /** Diagnostics are best-effort; even an accidentally async observer cannot own the loop. */
190
- function observeDiagnostic(callback: () => unknown, name: string): void {
208
+ function observeDiagnostic(callback: () => unknown, onFailure: (error: unknown) => void): void {
191
209
  try {
192
- void Promise.resolve(callback()).catch((error: unknown) => logBackgroundError(name, error));
210
+ void Promise.resolve(callback()).catch(onFailure);
193
211
  } catch (error) {
194
- logBackgroundError(name, error);
212
+ onFailure(error);
195
213
  }
196
214
  }
197
215
 
198
- function logBackgroundError(name: string, error: unknown): void {
199
- try { console.error(`[background:${name}]`, redactSensitive(error)); }
200
- catch { /* A broken diagnostic sink must not terminate the workload supervisor. */ }
216
+ /** One line per event: "background Crasher crashed" with the service name in the fields. */
217
+ function report(
218
+ diagnostics: HostedServiceDiagnostics | undefined,
219
+ level: "error" | "warn",
220
+ name: string,
221
+ message: string,
222
+ fields: Readonly<Record<string, unknown>>,
223
+ ): void {
224
+ try {
225
+ if (diagnostics) {
226
+ diagnostics[level](`background ${name} ${message}`, { service: name, ...fields });
227
+ return;
228
+ }
229
+ const { error, ...rest } = fields;
230
+ const details = error === undefined ? (Object.keys(rest).length ? [rest] : []) : [error];
231
+ (level === "error" ? console.error : console.warn)(`[background:${name}] ${message}`, ...details);
232
+ } catch { /* A broken diagnostic sink must not terminate the workload supervisor. */ }
201
233
  }
202
234
 
203
- async function waitWithTimeout(promise: Promise<void>, timeoutMs: number, name: string): Promise<void> {
235
+ async function waitWithTimeout(promise: Promise<void>, timeoutMs: number, onTimeout: () => void): Promise<void> {
204
236
  const guarded = promise.catch(() => undefined);
205
237
  if (timeoutMs <= 0) {
206
238
  await guarded;
@@ -209,7 +241,7 @@ async function waitWithTimeout(promise: Promise<void>, timeoutMs: number, name:
209
241
  let timer: ReturnType<typeof setTimeout> | undefined;
210
242
  const timeout = new Promise<void>((resolve) => {
211
243
  timer = setTimeout(() => {
212
- console.warn(`[background:${name}] did not stop within ${timeoutMs}ms; continuing shutdown`);
244
+ onTimeout();
213
245
  resolve();
214
246
  }, timeoutMs);
215
247
  });
@@ -13,9 +13,20 @@ export interface HostedServicePlanValidator {
13
13
  validate(services: readonly HostedService[], signal?: AbortSignal): void | Promise<void>;
14
14
  }
15
15
 
16
+ /**
17
+ * Where a hosted service reports what it handles itself: crashes, restarts, a
18
+ * slow stop. The application logger fits it structurally.
19
+ */
20
+ export interface HostedServiceDiagnostics {
21
+ error(message: string, fields?: Readonly<Record<string, unknown>>): void;
22
+ warn(message: string, fields?: Readonly<Record<string, unknown>>): void;
23
+ }
24
+
16
25
  export interface HostedService {
17
26
  /** Shared validators see real service identities, including inside supervised wrappers. */
18
27
  readonly planValidator?: HostedServicePlanValidator;
28
+ /** Receives the application logger before start, when one is registered. */
29
+ useDiagnostics?(diagnostics: HostedServiceDiagnostics): void;
19
30
  start(signal?: AbortSignal): void | Promise<void>;
20
31
  stop(): void | Promise<void>;
21
32
  /**
@@ -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 HostedServicePlanValidator } from "./hosted-service";
3
+ export { addHostedService, HOSTED_SERVICE, startHostedServices, stopHostedServices, type HostedService, type HostedServiceDiagnostics, type HostedServicePlanValidator } from "./hosted-service";
4
4
  export { SERVICE_PROVIDER } from "./service-provider-token";
5
5
  export {
6
6
  addOptions,
@@ -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.97.9" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.97.10" }, 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,6 +1,7 @@
1
1
  import { validateOptionsOnStart, type HostedService } from "../di";
2
2
  import type { Token } from "../di";
3
3
  import { LIFECYCLE_HOOK } from "./lifecycleHooks";
4
+ import { LOGGER } from "./logging/Logger";
4
5
  import type { LifecycleHook } from "./types";
5
6
  import { ShutdownTimeoutError } from "./errors";
6
7
  import { awaitAbortable } from "./internal/awaitAbortable";
@@ -9,6 +10,7 @@ import { awaitShutdown } from "./internal/awaitShutdown";
9
10
 
10
11
  interface LifecycleResolver {
11
12
  resolveAll<T>(token: Token<T>): readonly T[];
13
+ tryResolve?<T>(token: Token<T>): T | undefined;
12
14
  }
13
15
 
14
16
  /**
@@ -57,6 +59,9 @@ export class LifecycleCoordinator {
57
59
  signal?.throwIfAborted();
58
60
  validateOptionsOnStart(this.resolver);
59
61
  const hosted = resolveHostedServices(this.resolver);
62
+ // Background failures go to the application logger, like HTTP errors.
63
+ const logger = this.resolver.tryResolve?.(LOGGER);
64
+ if (logger) for (const service of hosted) service.useDiagnostics?.(logger);
60
65
  const validation = validateHostedServicePlan(hosted, signal);
61
66
  if (validation) await awaitAbortable(validation, signal);
62
67
  this.hooks = this.resolver.resolveAll(LIFECYCLE_HOOK);
@@ -1,4 +1,4 @@
1
- import type { HostedService, HostedServicePlanValidator } from "../di";
1
+ import type { HostedService, HostedServiceDiagnostics, HostedServicePlanValidator } from "../di";
2
2
  import { validateHostedServicePlan, registerHostedServiceWrapper, type HostedServicePlanAdmission } from "../di/extensions/hosted-service";
3
3
  import { awaitAbortable } from "./internal/awaitAbortable";
4
4
  import { reportDiagnosticFailure } from "./internal/reportDiagnosticFailure";
@@ -34,6 +34,7 @@ export class SupervisedHostedService implements HostedService {
34
34
  private admittedPhase?: number;
35
35
  private starting?: Promise<void>;
36
36
  private stopping?: Promise<void>;
37
+ private diagnostics?: HostedServiceDiagnostics;
37
38
 
38
39
  public constructor(
39
40
  private readonly factory: () => HostedService,
@@ -46,6 +47,11 @@ export class SupervisedHostedService implements HostedService {
46
47
  });
47
48
  }
48
49
 
50
+ /** Passed on to every attempt's service before it starts. */
51
+ public useDiagnostics(diagnostics: HostedServiceDiagnostics): void {
52
+ this.diagnostics = diagnostics;
53
+ }
54
+
49
55
  public start(signal?: AbortSignal): Promise<void> {
50
56
  if (this.stopping) {
51
57
  const stopping = this.stopping;
@@ -71,6 +77,7 @@ export class SupervisedHostedService implements HostedService {
71
77
  this.prepared = undefined;
72
78
  try {
73
79
  signal?.throwIfAborted();
80
+ if (this.diagnostics) service.useDiagnostics?.(this.diagnostics);
74
81
  await service.start(signal);
75
82
  signal?.throwIfAborted();
76
83
  this.inner = service;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.97.9",
3
+ "version": "0.97.10",
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",