bazis 0.105.2 → 0.106.0

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.
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../library/ui";
22
22
  import { modelValidatorAdapter } from "../../library/validation";
23
23
  import { createLocalize, type AppLanguage, type AppMessages } from "./messages";
24
+ import { useCodexLanguage } from "../infra/connectors/codex/contracts";
24
25
  import {
25
26
  type RunAppUiOptions,
26
27
  type RunAppUiSurfaceOptions,
@@ -290,6 +291,7 @@ export async function runApp(root: BazisModuleRef, options: RunAppOptions = {}):
290
291
  export function composeApp(root: BazisModuleRef, options: RunAppOptions = {}): BazisModuleRef {
291
292
  // Checked even without HTTP: an unknown language is a startup error.
292
293
  const localize = createLocalize(options.language, options.messages);
294
+ useCodexLanguage(options.language ?? "en");
293
295
  // Infrastructure and cache are global modules; add them to the graph next to the feature root.
294
296
  const globals: BazisModuleRef[] = [];
295
297
  if (options.infra !== undefined) {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AsyncResolutionRequiredError,
3
3
  ClassDependenciesMismatchError,
4
+ DiError,
4
5
  InvalidProviderError,
5
6
  NamedDependencyNotFoundError,
6
7
  ProviderNotFoundError,
@@ -44,7 +45,27 @@ import {
44
45
  import type { Token } from "./token";
45
46
  import { tokenToDebugName } from "./token";
46
47
 
48
+ /**
49
+ * `ServiceProvider` as a service in a scope sees that scope, as .NET's
50
+ * IServiceProvider does: `resolve` returns the scope's services and
51
+ * `createScope()` opens a new one. The scope is disposed by whoever created it.
52
+ */
53
+ class ScopeServiceProvider extends ServiceScope {
54
+ public constructor(provider: ConstructorParameters<typeof ServiceScope>[0], state: ResolutionScopeState, private readonly root: ServiceProvider) {
55
+ super(provider, state);
56
+ }
57
+
58
+ public createScope(): ServiceScope {
59
+ return this.root.createScope();
60
+ }
61
+
62
+ public override dispose(): Promise<void> {
63
+ return Promise.reject(new DiError("This ServiceProvider is the scope that resolved the service; whoever created the scope disposes it. To own a scope, call createScope() and dispose that one."));
64
+ }
65
+ }
66
+
47
67
  export class ServiceProvider implements ServiceResolver {
68
+ private readonly scopeProviders = new WeakMap<ResolutionScopeState, ScopeServiceProvider>();
48
69
  private readonly registry: ServiceRegistry;
49
70
  private readonly activations = new ResolutionTracker();
50
71
  private readonly lifetime: ScopeLifecycle;
@@ -185,6 +206,17 @@ export class ServiceProvider implements ServiceResolver {
185
206
  return this.lifetime.disposeScope(scopeState);
186
207
  }
187
208
 
209
+ /** `ServiceProvider` asked for inside a scope: the scope itself, not the root. */
210
+ private providerForScope<T>(token: Token<T>, key: ServiceKey | undefined, scopeState: ResolutionScopeState): T | undefined {
211
+ if (token !== (ServiceProvider as unknown) || key !== undefined || scopeState.isRoot) return undefined;
212
+ let view = this.scopeProviders.get(scopeState);
213
+ if (view === undefined) {
214
+ view = new ScopeServiceProvider(this, scopeState, this);
215
+ this.scopeProviders.set(scopeState, view);
216
+ }
217
+ return view as unknown as T;
218
+ }
219
+
188
220
  private resolveSingle<T>(
189
221
  token: Token<T>,
190
222
  key: ServiceKey | undefined,
@@ -192,6 +224,8 @@ export class ServiceProvider implements ServiceResolver {
192
224
  stack: ResolutionActivation[],
193
225
  ownerLifetime: ProviderLifetime | undefined,
194
226
  ): T {
227
+ const scopeProvider = this.providerForScope(token, key, scopeState);
228
+ if (scopeProvider !== undefined) return scopeProvider;
195
229
  const registration = this.registry.find(token, key);
196
230
  if (!registration) {
197
231
  throw new ProviderNotFoundError(token, this.describeResolutionPath(stack, token));
@@ -278,6 +312,8 @@ export class ServiceProvider implements ServiceResolver {
278
312
  stack: ResolutionActivation[],
279
313
  ownerLifetime?: ProviderLifetime,
280
314
  ): Promise<T> {
315
+ const scopeProvider = this.providerForScope(token, key, scopeState);
316
+ if (scopeProvider !== undefined) return scopeProvider;
281
317
  const registration = this.registry.find(token, key);
282
318
  if (!registration) {
283
319
  throw new ProviderNotFoundError(token, this.describeResolutionPath(stack, token));
@@ -733,6 +769,8 @@ export class ServiceProvider implements ServiceResolver {
733
769
  ownerLifetime: ProviderLifetime | undefined,
734
770
  ): T | undefined {
735
771
  this.lifetime.assertLive(scopeState);
772
+ const scopeProvider = this.providerForScope(token, key, scopeState);
773
+ if (scopeProvider !== undefined) return scopeProvider;
736
774
  const registration = this.registry.find(token, key);
737
775
  if (!registration) {
738
776
  return undefined;
@@ -1,6 +1,6 @@
1
1
  import { ServiceValidationError } from "../errors";
2
2
  import { missingNamedDependencyIssue } from "../errors/NamedDependencyNotFoundError";
3
- import { isLazyDependency, isNamedDependency, isOptionalDependency } from "../provider";
3
+ import { isClassProvider, isLazyDependency, isNamedDependency, isOptionalDependency } from "../provider";
4
4
  import type {
5
5
  KeyedDependency,
6
6
  NamedDependency,
@@ -99,7 +99,7 @@ export class GraphValidator {
99
99
  const inner = isLazy || isOptional ? dependency.inner : dependency;
100
100
  // An optional dependency on a name nothing registered is simply absent.
101
101
  if (isNamedDependency(inner) && !this.context.hasName(inner.name)) {
102
- if (!isOptional) issues.add(missingNamedDependencyIssue(inner.name, tokenToDebugName(registration.token)));
102
+ if (!isOptional) issues.add(missingNamedDependencyIssue(inner.name, serviceName(registration)));
103
103
  continue;
104
104
  }
105
105
  const descriptor = this.context.describeDependency(inner);
@@ -109,15 +109,15 @@ export class GraphValidator {
109
109
  }
110
110
  if (!depRegistration) {
111
111
  issues.add(
112
- `Missing dependency "${tokenToDebugName(descriptor.token)}" for "${tokenToDebugName(registration.token)}"`,
112
+ `Missing dependency "${tokenToDebugName(descriptor.token)}" for "${serviceName(registration)}"`,
113
113
  );
114
114
  continue;
115
115
  }
116
116
 
117
117
  if (this.context.validateScopes && rootLifetime === "singleton" && depRegistration.lifetime === "scoped") {
118
118
  issues.add(
119
- `Singleton "${tokenToDebugName(registration.token)}" depends on scoped "${tokenToDebugName(descriptor.token)}". `
120
- + `A scoped service lives for one request or scope: make "${tokenToDebugName(registration.token)}" scoped too, `
119
+ `Singleton "${serviceName(registration)}" depends on scoped "${tokenToDebugName(descriptor.token)}". `
120
+ + `A scoped service lives for one request or scope: make "${className(registration) ?? tokenToDebugName(registration.token)}" scoped too, `
121
121
  + `or inject ServiceProvider and resolve "${tokenToDebugName(descriptor.token)}" in a scope you create (provider.createScope()).`,
122
122
  );
123
123
  }
@@ -136,3 +136,17 @@ export class GraphValidator {
136
136
  }
137
137
  }
138
138
  }
139
+
140
+ /** The class behind a token registration, when it is not the token itself: `singleton(LIFECYCLE_HOOK, WarmUp)`. */
141
+ function className(registration: ServiceRegistration): string | undefined {
142
+ if (!isClassProvider(registration.provider)) return undefined;
143
+ const name = registration.provider.useClass.name;
144
+ return name && name !== tokenToDebugName(registration.token) ? name : undefined;
145
+ }
146
+
147
+ /** `WarmUp (as BazisLifecycleHook)` for a class under a token, else the token. */
148
+ function serviceName(registration: ServiceRegistration): string {
149
+ const token = tokenToDebugName(registration.token);
150
+ const name = className(registration);
151
+ return name ? `${name} (as ${token})` : token;
152
+ }
@@ -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.105.2" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.106.0" }, 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");
@@ -52,7 +52,7 @@ const EN_CODEX_MESSAGES: Readonly<Record<CodexErrorCode, string>> = Object.freez
52
52
  LOGIN_EXPIRED: "The sign-in window expired. Start signing in again.",
53
53
  });
54
54
 
55
- /** Russian texts for every code: `CodexError.useMessages(RU_CODEX_MESSAGES)` at startup. */
55
+ /** Russian texts for every code; `runApp({ language: "ru" })` turns them on. */
56
56
  export const RU_CODEX_MESSAGES: Readonly<Record<CodexErrorCode, string>> = Object.freeze({
57
57
  NOT_CONFIGURED: "Подключение ChatGPT не настроено на сервере.",
58
58
  UNAVAILABLE: "Codex App Server недоступен. Проверьте его установку и настройки.",
@@ -67,15 +67,31 @@ export const RU_CODEX_MESSAGES: Readonly<Record<CodexErrorCode, string>> = Objec
67
67
  LOGIN_EXPIRED: "Время для входа истекло. Начните вход заново.",
68
68
  });
69
69
 
70
+ const CODEX_LANGUAGES = { en: EN_CODEX_MESSAGES, ru: RU_CODEX_MESSAGES } as const;
71
+ let language: Readonly<Record<CodexErrorCode, string>> = EN_CODEX_MESSAGES;
72
+ let own: Partial<Record<CodexErrorCode, string>> = {};
70
73
  let messages: Readonly<Record<CodexErrorCode, string>> = EN_CODEX_MESSAGES;
71
74
 
72
- /** `message` is user-facing text for `code`; switch its language with `useMessages`. */
75
+ /**
76
+ * @internal `runApp({ language })` sets the language of the texts. It is
77
+ * process-wide, like the texts themselves: the last started application wins.
78
+ */
79
+ export function useCodexLanguage(name: keyof typeof CODEX_LANGUAGES): void {
80
+ language = CODEX_LANGUAGES[name];
81
+ messages = Object.freeze({ ...language, ...own });
82
+ }
83
+
84
+ /** `message` is user-facing text for `code`, in the language of `runApp({ language })`. */
73
85
  export class CodexError extends Error {
74
86
  constructor(readonly code: CodexErrorCode) { super(messages[code]); this.name = "CodexError"; }
75
87
 
76
- /** Replace texts for some or all codes (process-wide); missing codes keep English. */
88
+ /**
89
+ * Own texts for some or all codes (process-wide), on top of the application
90
+ * language; missing codes keep it. `useMessages({})` drops own texts.
91
+ */
77
92
  static useMessages(overrides: Partial<Record<CodexErrorCode, string>>): void {
78
- messages = Object.freeze({ ...EN_CODEX_MESSAGES, ...overrides });
93
+ own = { ...overrides };
94
+ messages = Object.freeze({ ...language, ...own });
79
95
  }
80
96
  }
81
97
  export const CODEX_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.105.2",
3
+ "version": "0.106.0",
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",