@cleverbrush/di 0.0.0-beta-20260410162047

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.
@@ -0,0 +1,274 @@
1
+ import type { FunctionSchemaBuilder, InferType, SchemaBuilder } from '@cleverbrush/schema';
2
+ import { ServiceProvider } from './ServiceProvider.js';
3
+ import type { ServiceFactory, ServiceRegistrationOptions } from './types.js';
4
+ /**
5
+ * Options for building a {@link ServiceProvider} from a {@link ServiceCollection}.
6
+ *
7
+ * @see {@link ServiceCollection.buildServiceProvider}
8
+ */
9
+ export interface ServiceProviderOptions {
10
+ /**
11
+ * When `true`, resolving a {@link ServiceLifetime.Scoped | Scoped} service
12
+ * from the root provider (outside of a scope) throws an error. This helps
13
+ * catch accidental singleton captures of scoped services.
14
+ *
15
+ * Recommended for development. Matches .NET's `ValidateScopes` behaviour.
16
+ *
17
+ * @defaultValue `true`
18
+ */
19
+ validateScopes?: boolean;
20
+ }
21
+ /**
22
+ * A mutable collection of service registrations. Build up the collection by
23
+ * calling {@link addSingleton}, {@link addScoped}, or {@link addTransient},
24
+ * then call {@link buildServiceProvider} to create an immutable
25
+ * {@link ServiceProvider}.
26
+ *
27
+ * Schema instances act as service keys via reference equality — the same
28
+ * schema object used during registration must be used during resolution.
29
+ *
30
+ * @example Basic registration and resolution
31
+ * ```ts
32
+ * import { ServiceCollection } from '@cleverbrush/di';
33
+ * import { object, string, number, func } from '@cleverbrush/schema';
34
+ *
35
+ * const IConfig = object({ port: number(), host: string() });
36
+ * const ILogger = object({ info: func() });
37
+ *
38
+ * const services = new ServiceCollection();
39
+ * services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
40
+ * services.addSingleton(ILogger, () => ({ info: console.log }));
41
+ *
42
+ * const provider = services.buildServiceProvider();
43
+ * const config = provider.get(IConfig); // { port: number; host: string }
44
+ * ```
45
+ *
46
+ * @example Function-schema-driven registration
47
+ * ```ts
48
+ * const IGreeter = object({ greet: func() });
49
+ * const greeterFactory = func()
50
+ * .addParameter(IConfig)
51
+ * .addParameter(ILogger);
52
+ *
53
+ * services.addSingletonFromSchema(
54
+ * IGreeter,
55
+ * greeterFactory,
56
+ * (config, logger) => ({
57
+ * greet() { logger.info(`Hello from ${config.host}`); }
58
+ * })
59
+ * );
60
+ * ```
61
+ *
62
+ * @see {@link ServiceProvider}
63
+ * @see {@link ServiceLifetime}
64
+ */
65
+ export declare class ServiceCollection {
66
+ #private;
67
+ /**
68
+ * Registers a service with {@link ServiceLifetime.Singleton | Singleton}
69
+ * lifetime. The factory (or value) is called at most once; every
70
+ * subsequent resolution returns the same instance.
71
+ *
72
+ * @param schema - The schema used as the service identifier.
73
+ * @param factoryOrValue - Either a {@link ServiceFactory} function that
74
+ * receives the provider, or a plain value (which is wrapped in a
75
+ * factory automatically).
76
+ * @param options - Optional {@link ServiceRegistrationOptions}.
77
+ * @returns `this` for chaining.
78
+ *
79
+ * @example Factory registration
80
+ * ```ts
81
+ * services.addSingleton(ILogger, (provider) => {
82
+ * const config = provider.get(IConfig);
83
+ * return new ConsoleLogger(config.logLevel);
84
+ * });
85
+ * ```
86
+ *
87
+ * @example Value shorthand
88
+ * ```ts
89
+ * services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
90
+ * ```
91
+ *
92
+ * @see {@link addScoped}
93
+ * @see {@link addTransient}
94
+ */
95
+ addSingleton<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema, factoryOrValue: ServiceFactory<InferType<TSchema>> | InferType<TSchema>, options?: ServiceRegistrationOptions): this;
96
+ /**
97
+ * Registers a service with {@link ServiceLifetime.Scoped | Scoped}
98
+ * lifetime. One instance is created per {@link ServiceScope}; within a
99
+ * scope every resolution returns the same instance.
100
+ *
101
+ * @param schema - The schema used as the service identifier.
102
+ * @param factory - A {@link ServiceFactory} function that creates the
103
+ * service instance. Unlike {@link addSingleton}, a plain value is
104
+ * **not** accepted because scoped services must be freshly created
105
+ * per scope.
106
+ * @param options - Optional {@link ServiceRegistrationOptions}.
107
+ * @returns `this` for chaining.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const IDbContext = object({ query: func() });
112
+ *
113
+ * services.addScoped(IDbContext, (provider) => {
114
+ * const config = provider.get(IConfig);
115
+ * return new DbContext(config.connectionString);
116
+ * });
117
+ * ```
118
+ *
119
+ * @see {@link addSingleton}
120
+ * @see {@link addTransient}
121
+ */
122
+ addScoped<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema, factory: ServiceFactory<InferType<TSchema>>, options?: ServiceRegistrationOptions): this;
123
+ /**
124
+ * Registers a service with {@link ServiceLifetime.Transient | Transient}
125
+ * lifetime. A new instance is created on every resolution.
126
+ *
127
+ * @param schema - The schema used as the service identifier.
128
+ * @param factory - A {@link ServiceFactory} function that creates the
129
+ * service instance.
130
+ * @param options - Optional {@link ServiceRegistrationOptions}.
131
+ * @returns `this` for chaining.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const IRequestId = object({ id: string() });
136
+ *
137
+ * services.addTransient(IRequestId, () => ({
138
+ * id: crypto.randomUUID()
139
+ * }));
140
+ * ```
141
+ *
142
+ * @see {@link addSingleton}
143
+ * @see {@link addScoped}
144
+ */
145
+ addTransient<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema, factory: ServiceFactory<InferType<TSchema>>, options?: ServiceRegistrationOptions): this;
146
+ /**
147
+ * Registers a pre-created instance as a {@link ServiceLifetime.Singleton | Singleton}.
148
+ * Unlike {@link addSingleton}, the second argument is always treated as the
149
+ * service value — never as a factory function. This makes it safe to
150
+ * register a function-typed service (e.g. a schema backed by `func()`)
151
+ * without the container accidentally invoking it as a factory.
152
+ *
153
+ * @param schema - The schema used as the service identifier.
154
+ * @param instance - The pre-created value to register. Can be any type
155
+ * including a function.
156
+ * @param options - Optional {@link ServiceRegistrationOptions}.
157
+ * @returns `this` for chaining.
158
+ *
159
+ * @example Register a plain object instance
160
+ * ```ts
161
+ * const config = { port: 3000, host: 'localhost' };
162
+ * services.addSingletonInstance(IConfig, config);
163
+ * ```
164
+ *
165
+ * @example Register a function value (impossible with {@link addSingleton})
166
+ * ```ts
167
+ * const IHandler = func();
168
+ * const myHandler = (req: Request) => new Response('ok');
169
+ * services.addSingletonInstance(IHandler, myHandler);
170
+ * ```
171
+ *
172
+ * @see {@link addSingleton}
173
+ */
174
+ addSingletonInstance<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema, instance: InferType<TSchema>, options?: ServiceRegistrationOptions): this;
175
+ /**
176
+ * Registers a {@link ServiceLifetime.Singleton | Singleton} service whose
177
+ * dependencies are described by a {@link FunctionSchemaBuilder}. The
178
+ * container resolves each parameter schema from the registry and passes
179
+ * the resolved values to `implementation`.
180
+ *
181
+ * @param targetSchema - The schema used as the service identifier for the
182
+ * created service.
183
+ * @param funcSchema - A {@link FunctionSchemaBuilder} whose
184
+ * `introspect().parameters` list the dependency schemas.
185
+ * @param implementation - A function whose parameters match the
186
+ * `funcSchema` parameter schemas (in order) and returns the service
187
+ * instance.
188
+ * @param options - Optional {@link ServiceRegistrationOptions}.
189
+ * @returns `this` for chaining.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * const IGreeter = object({ greet: func() });
194
+ *
195
+ * const greeterDeps = func()
196
+ * .addParameter(IConfig)
197
+ * .addParameter(ILogger);
198
+ *
199
+ * services.addSingletonFromSchema(
200
+ * IGreeter,
201
+ * greeterDeps,
202
+ * (config, logger) => ({
203
+ * greet() { logger.info(`Hello from ${config.host}`); }
204
+ * })
205
+ * );
206
+ * ```
207
+ *
208
+ * @see {@link addScopedFromSchema}
209
+ * @see {@link addTransientFromSchema}
210
+ */
211
+ addSingletonFromSchema<TTargetSchema extends SchemaBuilder<any, any, any, any, any>, TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>>(targetSchema: TTargetSchema, funcSchema: TFuncSchema, implementation: (...args: FuncSchemaParameters<TFuncSchema>) => InferType<TTargetSchema>, options?: ServiceRegistrationOptions): this;
212
+ /**
213
+ * Registers a {@link ServiceLifetime.Scoped | Scoped} service whose
214
+ * dependencies are described by a {@link FunctionSchemaBuilder}.
215
+ *
216
+ * @param targetSchema - The schema used as the service identifier.
217
+ * @param funcSchema - A {@link FunctionSchemaBuilder} describing the
218
+ * dependency schemas via its parameters.
219
+ * @param implementation - A function receiving the resolved dependencies
220
+ * and returning the service instance.
221
+ * @param options - Optional {@link ServiceRegistrationOptions}.
222
+ * @returns `this` for chaining.
223
+ *
224
+ * @see {@link addSingletonFromSchema}
225
+ * @see {@link addTransientFromSchema}
226
+ */
227
+ addScopedFromSchema<TTargetSchema extends SchemaBuilder<any, any, any, any, any>, TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>>(targetSchema: TTargetSchema, funcSchema: TFuncSchema, implementation: (...args: FuncSchemaParameters<TFuncSchema>) => InferType<TTargetSchema>, options?: ServiceRegistrationOptions): this;
228
+ /**
229
+ * Registers a {@link ServiceLifetime.Transient | Transient} service whose
230
+ * dependencies are described by a {@link FunctionSchemaBuilder}.
231
+ *
232
+ * @param targetSchema - The schema used as the service identifier.
233
+ * @param funcSchema - A {@link FunctionSchemaBuilder} describing the
234
+ * dependency schemas via its parameters.
235
+ * @param implementation - A function receiving the resolved dependencies
236
+ * and returning the service instance.
237
+ * @param options - Optional {@link ServiceRegistrationOptions}.
238
+ * @returns `this` for chaining.
239
+ *
240
+ * @see {@link addSingletonFromSchema}
241
+ * @see {@link addScopedFromSchema}
242
+ */
243
+ addTransientFromSchema<TTargetSchema extends SchemaBuilder<any, any, any, any, any>, TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>>(targetSchema: TTargetSchema, funcSchema: TFuncSchema, implementation: (...args: FuncSchemaParameters<TFuncSchema>) => InferType<TTargetSchema>, options?: ServiceRegistrationOptions): this;
244
+ /**
245
+ * Creates a new {@link ServiceProvider} from the registrations currently
246
+ * contained in this collection.
247
+ *
248
+ * @param options - Optional {@link ServiceProviderOptions} to control
249
+ * provider behaviour (e.g. scope validation).
250
+ * @returns A new {@link ServiceProvider} instance.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * const provider = services.buildServiceProvider();
255
+ * const logger = provider.get(ILogger);
256
+ * ```
257
+ *
258
+ * @example With scope validation disabled
259
+ * ```ts
260
+ * const provider = services.buildServiceProvider({
261
+ * validateScopes: false
262
+ * });
263
+ * ```
264
+ */
265
+ buildServiceProvider(options?: ServiceProviderOptions): ServiceProvider;
266
+ }
267
+ /**
268
+ * Extracts the parameter types from a {@link FunctionSchemaBuilder} as a tuple.
269
+ * Used internally to type-check `addSingletonFromSchema`-style registrations.
270
+ */
271
+ type FuncSchemaParameters<T extends FunctionSchemaBuilder<any, any, any, any, any, any>> = T extends FunctionSchemaBuilder<any, any, any, any, any, infer TParams extends SchemaBuilder<any, any, any, any, any>[]> ? {
272
+ [K in keyof TParams]: InferType<TParams[K]>;
273
+ } : never;
274
+ export {};
@@ -0,0 +1,164 @@
1
+ import type { FunctionSchemaBuilder, InferType, SchemaBuilder } from '@cleverbrush/schema';
2
+ import type { ServiceProviderOptions } from './ServiceCollection.js';
3
+ import { ServiceScope } from './ServiceScope.js';
4
+ import type { IServiceProvider, ServiceDescriptor } from './types.js';
5
+ /**
6
+ * An immutable service provider that resolves services from a frozen set of
7
+ * {@link ServiceDescriptor | descriptors}. Obtain an instance by calling
8
+ * {@link ServiceCollection.buildServiceProvider}.
9
+ *
10
+ * Supports three service lifetimes:
11
+ * - **Singleton** — one instance per provider, cached after first resolution.
12
+ * - **Scoped** — one instance per {@link ServiceScope}, created via
13
+ * {@link createScope}.
14
+ * - **Transient** — a new instance on every call to {@link get}.
15
+ *
16
+ * @example Resolving services
17
+ * ```ts
18
+ * const provider = services.buildServiceProvider();
19
+ *
20
+ * // Typed via the schema — no explicit generic needed
21
+ * const config = provider.get(IConfig);
22
+ * config.port; // number
23
+ *
24
+ * // Optional resolution (returns undefined if not registered)
25
+ * const maybeMail = provider.getOptional(IMailer);
26
+ * ```
27
+ *
28
+ * @example Using scopes
29
+ * ```ts
30
+ * // Per-request scope (e.g. in an HTTP handler)
31
+ * using scope = provider.createScope();
32
+ * const db = scope.serviceProvider.get(IDbContext);
33
+ * // db is disposed when scope exits
34
+ * ```
35
+ *
36
+ * @example Function injection
37
+ * ```ts
38
+ * const handlerSchema = func()
39
+ * .addParameter(ILogger)
40
+ * .addParameter(IDbContext)
41
+ * .hasReturnType(string());
42
+ *
43
+ * const result = provider.invoke(handlerSchema, (logger, db) => {
44
+ * logger.info('Handling request');
45
+ * return db.query('SELECT 1');
46
+ * });
47
+ * ```
48
+ *
49
+ * @see {@link ServiceCollection}
50
+ * @see {@link ServiceScope}
51
+ */
52
+ export declare class ServiceProvider implements IServiceProvider {
53
+ #private;
54
+ /**
55
+ * @hidden
56
+ */
57
+ constructor(descriptors: Map<SchemaBuilder<any, any, any, any, any>, ServiceDescriptor>, options?: ServiceProviderOptions);
58
+ /**
59
+ * Resolves a service by its schema key.
60
+ *
61
+ * - **Singleton**: returns the cached instance, or creates and caches it.
62
+ * - **Scoped**: throws if called from the root provider when
63
+ * `validateScopes` is `true` (default). Use
64
+ * {@link createScope} to obtain a scoped provider.
65
+ * - **Transient**: creates a new instance on every call.
66
+ *
67
+ * @param schema - The schema instance used as the service identifier.
68
+ * Must be the same reference used during registration.
69
+ * @returns The resolved service, typed as `InferType<typeof schema>`.
70
+ * @throws {Error} If the schema is not registered.
71
+ * @throws {Error} If a scoped service is resolved from the root provider
72
+ * with `validateScopes` enabled.
73
+ * @throws {Error} If a circular dependency is detected.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const logger = provider.get(ILogger);
78
+ * logger.info('Hello'); // fully typed
79
+ * ```
80
+ */
81
+ get<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema>;
82
+ /**
83
+ * Resolves a service by its schema key, or returns `undefined` if the
84
+ * schema is not registered.
85
+ *
86
+ * Unlike {@link get}, this method does **not** throw for unregistered
87
+ * schemas. All other behaviour (lifetime, scope validation, circular
88
+ * dependency detection) is the same.
89
+ *
90
+ * @param schema - The schema instance used as the service identifier.
91
+ * @returns The resolved service or `undefined`.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const mailer = provider.getOptional(IMailer);
96
+ * if (mailer) {
97
+ * mailer.send('test@example.com', 'Hello');
98
+ * }
99
+ * ```
100
+ */
101
+ getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema> | undefined;
102
+ /**
103
+ * Creates a new {@link ServiceScope}. Scoped services resolved from the
104
+ * scope's {@link ServiceScope.serviceProvider | serviceProvider} are
105
+ * cached per-scope and disposed when the scope is disposed.
106
+ *
107
+ * The returned scope implements both `Symbol.dispose` and
108
+ * `Symbol.asyncDispose`, so it works with the `using` keyword:
109
+ *
110
+ * @returns A new {@link ServiceScope}.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * // Sync disposal
115
+ * using scope = provider.createScope();
116
+ * const db = scope.serviceProvider.get(IDbContext);
117
+ *
118
+ * // Async disposal
119
+ * await using scope = provider.createScope();
120
+ * const db = scope.serviceProvider.get(IDbContext);
121
+ * ```
122
+ */
123
+ createScope(): ServiceScope;
124
+ /**
125
+ * Resolves the dependencies described by a {@link FunctionSchemaBuilder}
126
+ * and calls `implementation` with the resolved values.
127
+ *
128
+ * Each parameter schema in `funcSchema.introspect().parameters` is
129
+ * resolved from this provider. The implementation receives the resolved
130
+ * values in the same order as the parameter schemas.
131
+ *
132
+ * @param funcSchema - A {@link FunctionSchemaBuilder} whose parameters
133
+ * describe the services to inject.
134
+ * @param implementation - A function whose parameters match the
135
+ * `funcSchema` parameter types and whose return type matches the
136
+ * `funcSchema` return type.
137
+ * @returns The return value of `implementation`.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const handler = func()
142
+ * .addParameter(ILogger)
143
+ * .addParameter(IConfig)
144
+ * .hasReturnType(string());
145
+ *
146
+ * const result = provider.invoke(handler, (logger, config) => {
147
+ * logger.info(`Running on port ${config.port}`);
148
+ * return 'ok';
149
+ * });
150
+ * // result: string
151
+ * ```
152
+ *
153
+ * @see {@link FunctionSchemaBuilder.addParameter}
154
+ * @see {@link FunctionSchemaBuilder.hasReturnType}
155
+ */
156
+ invoke<TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>>(funcSchema: TFuncSchema, implementation: InferType<TFuncSchema>): ReturnType<InferType<TFuncSchema>>;
157
+ /**
158
+ * Resolves a service within a scope context. Called by
159
+ * {@link ScopedServiceProvider}.
160
+ *
161
+ * @hidden
162
+ */
163
+ resolveScoped(schema: SchemaBuilder<any, any, any, any, any>, resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>, scopedCache: Map<SchemaBuilder<any, any, any, any, any>, any>, trackDisposable: (instance: any) => void): any;
164
+ }
@@ -0,0 +1,201 @@
1
+ import type { FunctionSchemaBuilder, InferType, SchemaBuilder } from '@cleverbrush/schema';
2
+ import type { IServiceProvider, ServiceDescriptor } from './types.js';
3
+ /**
4
+ * A scoped service container that caches {@link ServiceLifetime.Scoped | Scoped}
5
+ * services for the duration of its lifetime. Obtain instances via
6
+ * {@link ServiceProvider.createScope}.
7
+ *
8
+ * The scope tracks all created scoped services that implement `Disposable` or
9
+ * `AsyncDisposable` and disposes them in reverse creation order (LIFO) when the
10
+ * scope is disposed. This matches .NET's scope disposal semantics.
11
+ *
12
+ * Supports the `using` keyword via `Symbol.dispose` and `Symbol.asyncDispose`:
13
+ *
14
+ * @example Synchronous disposal
15
+ * ```ts
16
+ * using scope = provider.createScope();
17
+ * const db = scope.serviceProvider.get(IDbContext);
18
+ * // db is automatically disposed when the block exits
19
+ * ```
20
+ *
21
+ * @example Asynchronous disposal
22
+ * ```ts
23
+ * await using scope = provider.createScope();
24
+ * const db = scope.serviceProvider.get(IDbContext);
25
+ * // db is asynchronously disposed when the block exits
26
+ * ```
27
+ *
28
+ * @example Manual disposal
29
+ * ```ts
30
+ * const scope = provider.createScope();
31
+ * try {
32
+ * const db = scope.serviceProvider.get(IDbContext);
33
+ * // use db...
34
+ * } finally {
35
+ * await scope.asyncDispose();
36
+ * }
37
+ * ```
38
+ *
39
+ * @see {@link ServiceProvider.createScope}
40
+ * @see {@link ServiceLifetime.Scoped}
41
+ */
42
+ export declare class ServiceScope implements Disposable, AsyncDisposable {
43
+ #private;
44
+ /**
45
+ * A child {@link IServiceProvider} that resolves services within this scope.
46
+ *
47
+ * - **Singleton** services are resolved from the root provider's cache.
48
+ * - **Scoped** services are resolved from this scope's cache (created on
49
+ * first access, reused within the scope).
50
+ * - **Transient** services create a new instance on every call.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const scope = provider.createScope();
55
+ * const db = scope.serviceProvider.get(IDbContext); // scoped
56
+ * const logger = scope.serviceProvider.get(ILogger); // singleton from root
57
+ * ```
58
+ */
59
+ readonly serviceProvider: ScopedServiceProvider;
60
+ /**
61
+ * @hidden
62
+ */
63
+ constructor(descriptors: Map<SchemaBuilder<any, any, any, any, any>, ServiceDescriptor>, singletonCache: Map<SchemaBuilder<any, any, any, any, any>, any>);
64
+ /**
65
+ * Synchronously disposes all scoped services that implement
66
+ * `Symbol.dispose`, in reverse creation order (LIFO).
67
+ *
68
+ * Services that only implement `Symbol.asyncDispose` are skipped during
69
+ * synchronous disposal. Use {@link asyncDispose} to properly dispose all
70
+ * services including async ones.
71
+ *
72
+ * @throws Re-throws the first error encountered during disposal, but
73
+ * still attempts to dispose remaining services.
74
+ */
75
+ dispose(): void;
76
+ /**
77
+ * Asynchronously disposes all scoped services that implement
78
+ * `Symbol.asyncDispose` or `Symbol.dispose`, in reverse creation order
79
+ * (LIFO).
80
+ *
81
+ * For each service:
82
+ * - If it implements `Symbol.asyncDispose`, that method is awaited.
83
+ * - Otherwise, if it implements `Symbol.dispose`, that method is called
84
+ * synchronously.
85
+ *
86
+ * @throws Re-throws the first error encountered during disposal, but
87
+ * still attempts to dispose remaining services.
88
+ */
89
+ asyncDispose(): Promise<void>;
90
+ /**
91
+ * Implements the synchronous `Disposable` protocol. Delegates to
92
+ * {@link dispose}.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * using scope = provider.createScope();
97
+ * ```
98
+ */
99
+ [Symbol.dispose](): void;
100
+ /**
101
+ * Implements the asynchronous `AsyncDisposable` protocol. Delegates to
102
+ * {@link asyncDispose}.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * await using scope = provider.createScope();
107
+ * ```
108
+ */
109
+ [Symbol.asyncDispose](): Promise<void>;
110
+ /**
111
+ * @hidden
112
+ */
113
+ getDescriptors(): Map<SchemaBuilder<any, any, any, any, any>, ServiceDescriptor>;
114
+ /**
115
+ * @hidden
116
+ */
117
+ getSingletonCache(): Map<SchemaBuilder<any, any, any, any, any>, any>;
118
+ /**
119
+ * @hidden
120
+ */
121
+ getScopedCache(): Map<SchemaBuilder<any, any, any, any, any>, any>;
122
+ /**
123
+ * @hidden
124
+ */
125
+ trackDisposable(instance: any): void;
126
+ }
127
+ /**
128
+ * A service provider scoped to a {@link ServiceScope}. Resolves scoped
129
+ * services from the scope's cache and singletons from the root cache.
130
+ *
131
+ * This class is not instantiated directly — obtain it via
132
+ * `scope.serviceProvider`.
133
+ *
134
+ * @see {@link ServiceScope}
135
+ */
136
+ export declare class ScopedServiceProvider implements IServiceProvider {
137
+ #private;
138
+ /**
139
+ * @hidden
140
+ */
141
+ constructor(scope: ServiceScope);
142
+ /**
143
+ * Resolves a service within this scope.
144
+ *
145
+ * - **Singleton** services are resolved from the root provider's cache.
146
+ * - **Scoped** services are created once per scope and cached.
147
+ * - **Transient** services are created fresh on every call.
148
+ *
149
+ * @param schema - The schema instance used as the service identifier.
150
+ * @returns The resolved service, typed as `InferType<typeof schema>`.
151
+ * @throws {Error} If the schema is not registered.
152
+ * @throws {Error} If a circular dependency is detected.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * using scope = provider.createScope();
157
+ * const db = scope.serviceProvider.get(IDbContext);
158
+ * ```
159
+ */
160
+ get<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema>;
161
+ /**
162
+ * Resolves a service within this scope, or returns `undefined` if not
163
+ * registered.
164
+ *
165
+ * @param schema - The schema instance used as the service identifier.
166
+ * @returns The resolved service or `undefined`.
167
+ */
168
+ getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema> | undefined;
169
+ /**
170
+ * Resolves the dependencies described by a {@link FunctionSchemaBuilder}
171
+ * and calls `implementation` with the resolved values. Scoped services
172
+ * are resolved within this scope.
173
+ *
174
+ * @param funcSchema - A {@link FunctionSchemaBuilder} whose parameters
175
+ * describe the services to inject.
176
+ * @param implementation - A function whose parameters match the
177
+ * `funcSchema` parameter types.
178
+ * @returns The return value of `implementation`.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * using scope = provider.createScope();
183
+ * const result = scope.serviceProvider.invoke(handler, (logger, db) => {
184
+ * logger.info('Handling request');
185
+ * return db.query('SELECT 1');
186
+ * });
187
+ * ```
188
+ *
189
+ * @see {@link ServiceProvider.invoke}
190
+ */
191
+ invoke<TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>>(funcSchema: TFuncSchema, implementation: InferType<TFuncSchema>): ReturnType<InferType<TFuncSchema>>;
192
+ /**
193
+ * Internal resolution entry point that reuses an existing resolution
194
+ * stack. Called by {@link ScopedResolverProxy} to propagate the stack
195
+ * across factory-to-factory calls, enabling circular dependency
196
+ * detection across the full chain.
197
+ *
198
+ * @hidden
199
+ */
200
+ resolveWithStack(schema: SchemaBuilder<any, any, any, any, any>, resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>): any;
201
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @cleverbrush/di — .NET-style dependency injection for TypeScript.
3
+ *
4
+ * Uses `@cleverbrush/schema` instances as service keys for type-safe
5
+ * registration and resolution. Supports singleton, scoped, and transient
6
+ * lifetimes, function injection via {@link FunctionSchemaBuilder}, and
7
+ * automatic disposal of scoped services.
8
+ *
9
+ * @example Quick start
10
+ * ```ts
11
+ * import { ServiceCollection } from '@cleverbrush/di';
12
+ * import { object, string, number, func } from '@cleverbrush/schema';
13
+ *
14
+ * // Define service contracts as schemas
15
+ * const IConfig = object({ port: number(), host: string() });
16
+ * const ILogger = object({ info: func().addParameter(string()) });
17
+ *
18
+ * // Register services
19
+ * const services = new ServiceCollection();
20
+ * services.addSingleton(IConfig, { port: 3000, host: 'localhost' });
21
+ * services.addSingleton(ILogger, () => ({ info: console.log }));
22
+ *
23
+ * // Build the provider
24
+ * const provider = services.buildServiceProvider();
25
+ *
26
+ * // Resolve — fully typed, no generics needed
27
+ * const config = provider.get(IConfig);
28
+ * config.port; // number
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+ export { ServiceCollection, type ServiceProviderOptions } from './ServiceCollection.js';
34
+ export { ServiceProvider } from './ServiceProvider.js';
35
+ export { ScopedServiceProvider, ServiceScope } from './ServiceScope.js';
36
+ export { type IServiceProvider, isAsyncDisposable, isDisposable, type ServiceDescriptor, type ServiceFactory, ServiceLifetime, type ServiceRegistrationOptions } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ var l=(a=>(a.Transient="Transient",a.Scoped="Scoped",a.Singleton="Singleton",a))(l||{});function s(y){return y!=null&&typeof y=="object"&&Symbol.dispose in y&&typeof y[Symbol.dispose]=="function"}function d(y){return y!=null&&typeof y=="object"&&Symbol.asyncDispose in y&&typeof y[Symbol.asyncDispose]=="function"}var o=class{#e;#n;#r=new Map;#a=[];#i=!1;serviceProvider;constructor(e,n){this.#e=e,this.#n=n,this.serviceProvider=new S(this)}dispose(){if(this.#i)return;this.#i=!0;let e;for(let n=this.#a.length-1;n>=0;n--){let a=this.#a[n];try{s(a)&&a[Symbol.dispose]()}catch(r){e||(e=r)}}if(this.#r.clear(),e)throw e}async asyncDispose(){if(this.#i)return;this.#i=!0;let e;for(let n=this.#a.length-1;n>=0;n--){let a=this.#a[n];try{d(a)?await a[Symbol.asyncDispose]():s(a)&&a[Symbol.dispose]()}catch(r){e||(e=r)}}if(this.#r.clear(),e)throw e}[Symbol.dispose](){this.dispose()}async[Symbol.asyncDispose](){await this.asyncDispose()}getDescriptors(){return this.#e}getSingletonCache(){return this.#n}getScopedCache(){return this.#r}trackDisposable(e){(s(e)||d(e))&&this.#a.push(e)}},S=class{#e;constructor(e){this.#e=e}get(e){return this.#n(e,new Set)}getOptional(e){if(this.#e.getDescriptors().has(e))return this.#n(e,new Set)}invoke(e,n){let a=e.introspect().parameters,r=new Set,i=a.map(t=>this.#n(t,r));return n.apply(null,i)}#n(e,n){let r=this.#e.getDescriptors().get(e);if(!r)throw new Error("Service not registered. The schema passed to get() was not registered in the ServiceCollection. Ensure you are using the same schema reference for registration and resolution.");if(n.has(e)){let i=[...n].map(t=>t.introspect().type??"(anonymous)").join(" \u2192 ");throw new Error(`Circular dependency detected: ${i} \u2192 ${r.schema.introspect().type??"(anonymous)"}`)}switch(r.lifetime){case"Singleton":{let i=this.#e.getSingletonCache();if(i.has(e))return i.get(e);n.add(e);let t=this.#r(r,n);return n.delete(e),i.set(e,t),t}case"Scoped":{let i=this.#e.getScopedCache();if(i.has(e))return i.get(e);n.add(e);let t=this.#r(r,n);return n.delete(e),i.set(e,t),this.#e.trackDisposable(t),t}case"Transient":{n.add(e);let i=this.#r(r,n);return n.delete(e),i}}}resolveWithStack(e,n){return this.#n(e,n)}#r(e,n){let a=new u(this,n),r=e.factory(a);if(e.validate){let i=e.schema.validate(r);if(!i.valid){let t=i.errors?.map(c=>c.message).join("; ")??"unknown error";throw new Error(`Service validation failed for schema type "${e.schema.introspect().type??"(anonymous)"}": ${t}`)}}return r}},u=class{#e;#n;constructor(e,n){this.#e=e,this.#n=n}get(e){return this.#e.resolveWithStack(e,this.#n)}getOptional(e){return this.#e.getOptional(e)}};var h=class{#e;#n=new Map;#r;constructor(e,n){this.#e=e,this.#r=n?.validateScopes??!0}get(e){return this.#a(e,new Set)}getOptional(e){if(this.#e.has(e))return this.#a(e,new Set)}createScope(){return new o(this.#e,this.#n)}invoke(e,n){let a=e.introspect().parameters,r=new Set,i=a.map(t=>this.#a(t,r));return n.apply(null,i)}#a(e,n,a,r){let i=this.#e.get(e);if(!i)throw new Error("Service not registered. The schema passed to get() was not registered in the ServiceCollection. Ensure you are using the same schema reference for registration and resolution.");if(n.has(e)){let t=[...n].map(c=>c.introspect().type??"(anonymous)").join(" \u2192 ");throw new Error(`Circular dependency detected: ${t} \u2192 ${i.schema.introspect().type??"(anonymous)"}`)}switch(i.lifetime){case"Singleton":{if(this.#n.has(e))return this.#n.get(e);n.add(e);let t=this.#i(i,n,a,r);return n.delete(e),this.#n.set(e,t),t}case"Scoped":{if(!a){if(this.#r)throw new Error("Cannot resolve scoped service from the root provider. Create a scope first with provider.createScope(). If this is intentional, set validateScopes: false in buildServiceProvider() options.");n.add(e);let c=this.#i(i,n,a,r);return n.delete(e),c}if(a.has(e))return a.get(e);n.add(e);let t=this.#i(i,n,a,r);return n.delete(e),a.set(e,t),r&&r(t),t}case"Transient":{n.add(e);let t=this.#i(i,n,a,r);return n.delete(e),t}}}#i(e,n,a,r){let i=new T(this,n,a,r),t=e.factory(i);if(e.validate){let c=e.schema.validate(t);if(!c.valid){let p=c.errors?.map(m=>m.message).join("; ")??"unknown error";throw new Error(`Service validation failed for schema type "${e.schema.introspect().type??"(anonymous)"}": ${p}`)}}return t}resolveScoped(e,n,a,r){return this.#a(e,n,a,r)}},T=class{#e;#n;#r;#a;constructor(e,n,a,r){this.#e=e,this.#n=n,this.#r=a,this.#a=r}get(e){return this.#e.resolveScoped(e,this.#n,this.#r??new Map,this.#a??(()=>{}))}getOptional(e){try{return this.get(e)}catch{return}}};var v=class{#e=new Map;addSingleton(e,n,a){return this.#n(e,"Singleton",n,a)}addScoped(e,n,a){return this.#n(e,"Scoped",n,a)}addTransient(e,n,a){return this.#n(e,"Transient",n,a)}addSingletonInstance(e,n,a){return this.#r(e,n,a)}addSingletonFromSchema(e,n,a,r){return this.#a(e,n,a,"Singleton",r)}addScopedFromSchema(e,n,a,r){return this.#a(e,n,a,"Scoped",r)}addTransientFromSchema(e,n,a,r){return this.#a(e,n,a,"Transient",r)}buildServiceProvider(e){let n=new Map(this.#e);return new h(n,e)}#n(e,n,a,r){let i=typeof a=="function"?a:()=>a;return this.#e.set(e,{schema:e,lifetime:n,factory:i,validate:r?.validate??!1}),this}#r(e,n,a){let r=()=>n;return this.#e.set(e,{schema:e,lifetime:"Singleton",factory:r,validate:a?.validate??!1}),this}#a(e,n,a,r,i){let t=n.introspect().parameters,c=p=>{let m=t.map(f=>p.get(f));return a.apply(null,m)};return this.#e.set(e,{schema:e,lifetime:r,factory:c,validate:i?.validate??!1}),this}};export{S as ScopedServiceProvider,v as ServiceCollection,l as ServiceLifetime,h as ServiceProvider,o as ServiceScope,d as isAsyncDisposable,s as isDisposable};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/ServiceScope.ts","../src/ServiceProvider.ts","../src/ServiceCollection.ts"],"sourcesContent":["import type { InferType, SchemaBuilder } from '@cleverbrush/schema';\n\n/**\n * Defines the lifetime of a service within the dependency injection container.\n *\n * The lifetime determines when a new instance is created versus when a cached\n * instance is returned.\n *\n * @example\n * ```ts\n * import { ServiceCollection, ServiceLifetime } from '@cleverbrush/di';\n * import { object, string } from '@cleverbrush/schema';\n *\n * const ILogger = object({ info: func() });\n *\n * const services = new ServiceCollection();\n * // Equivalent to services.addSingleton(ILogger, ...)\n * services.add(ILogger, () => console, ServiceLifetime.Singleton);\n * ```\n *\n * @see {@link ServiceCollection}\n */\nexport enum ServiceLifetime {\n /**\n * A new instance is created every time the service is resolved.\n *\n * Use for lightweight, stateless services.\n */\n Transient = 'Transient',\n\n /**\n * One instance is created per {@link ServiceScope}. The same instance is\n * returned for every resolution within that scope.\n *\n * Use for services that should be shared within a single unit of work\n * (e.g. an HTTP request) but isolated between units.\n */\n Scoped = 'Scoped',\n\n /**\n * One instance is created for the entire lifetime of the\n * {@link ServiceProvider}. All subsequent resolutions return the same instance.\n *\n * Use for expensive-to-create or truly global services (loggers,\n * configuration, connection pools).\n */\n Singleton = 'Singleton'\n}\n\n/**\n * A factory function that creates a service instance, optionally resolving\n * other services from the provided {@link ServiceProvider}.\n *\n * @typeParam T - The type of the service instance the factory creates.\n *\n * @example\n * ```ts\n * const loggerFactory: ServiceFactory<Logger> = (provider) => {\n * const config = provider.get(IConfig);\n * return new Logger(config.logLevel);\n * };\n * ```\n *\n * @see {@link ServiceCollection}\n */\nexport type ServiceFactory<T> = (provider: IServiceProvider) => T;\n\n/**\n * Describes a registered service: its schema key, lifetime, factory, and\n * optional runtime validation flag.\n *\n * @typeParam T - The type of the service instance.\n *\n * @see {@link ServiceCollection}\n * @see {@link ServiceLifetime}\n */\nexport interface ServiceDescriptor<T = any> {\n /** The schema used as the service identifier (reference equality). */\n readonly schema: SchemaBuilder<T, any, any, any, any>;\n\n /** The lifetime of the service. */\n readonly lifetime: ServiceLifetime;\n\n /** The factory function that creates the service instance. */\n readonly factory: ServiceFactory<T>;\n\n /**\n * When `true`, the container validates the factory's return value against\n * the schema at resolution time using `schema.validate()`.\n *\n * @defaultValue `false`\n */\n readonly validate: boolean;\n}\n\n/**\n * Options for service registration.\n *\n * @see {@link ServiceCollection.addSingleton}\n * @see {@link ServiceCollection.addScoped}\n * @see {@link ServiceCollection.addTransient}\n */\nexport interface ServiceRegistrationOptions {\n /**\n * When `true`, the resolved value is validated against the schema at\n * resolution time. Useful for development/debugging. Has a performance\n * cost proportional to the schema complexity.\n *\n * @defaultValue `false`\n */\n validate?: boolean;\n}\n\n/**\n * Minimal interface for the service provider, used to avoid circular\n * dependencies between modules.\n *\n * @see {@link ServiceProvider}\n */\nexport interface IServiceProvider {\n /**\n * Resolves a service by its schema key.\n * @param schema - The schema instance used as the service identifier.\n * @throws If the service is not registered.\n */\n get<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema>;\n\n /**\n * Resolves a service by its schema key, or returns `undefined` if not registered.\n * @param schema - The schema instance used as the service identifier.\n */\n getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> | undefined;\n}\n\n/**\n * Checks whether a value implements the synchronous `Disposable` protocol\n * (`Symbol.dispose`).\n *\n * @param value - The value to check.\n * @returns `true` if `value` has a `Symbol.dispose` method.\n *\n * @example\n * ```ts\n * const resource = {\n * [Symbol.dispose]() { console.log('disposed'); }\n * };\n * isDisposable(resource); // true\n * isDisposable({}); // false\n * ```\n */\nexport function isDisposable(value: unknown): value is Disposable {\n return (\n value != null &&\n typeof value === 'object' &&\n Symbol.dispose in (value as object) &&\n typeof (value as Record<symbol, unknown>)[Symbol.dispose] === 'function'\n );\n}\n\n/**\n * Checks whether a value implements the asynchronous `Disposable` protocol\n * (`Symbol.asyncDispose`).\n *\n * @param value - The value to check.\n * @returns `true` if `value` has a `Symbol.asyncDispose` method.\n *\n * @example\n * ```ts\n * const connection = {\n * async [Symbol.asyncDispose]() { await db.close(); }\n * };\n * isAsyncDisposable(connection); // true\n * isAsyncDisposable({}); // false\n * ```\n */\nexport function isAsyncDisposable(value: unknown): value is AsyncDisposable {\n return (\n value != null &&\n typeof value === 'object' &&\n Symbol.asyncDispose in (value as object) &&\n typeof (value as Record<symbol, unknown>)[Symbol.asyncDispose] ===\n 'function'\n );\n}\n","import type {\n FunctionSchemaBuilder,\n InferType,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type { ServiceProvider } from './ServiceProvider.js';\nimport type { IServiceProvider, ServiceDescriptor } from './types.js';\nimport { isAsyncDisposable, isDisposable, ServiceLifetime } from './types.js';\n\n/**\n * A scoped service container that caches {@link ServiceLifetime.Scoped | Scoped}\n * services for the duration of its lifetime. Obtain instances via\n * {@link ServiceProvider.createScope}.\n *\n * The scope tracks all created scoped services that implement `Disposable` or\n * `AsyncDisposable` and disposes them in reverse creation order (LIFO) when the\n * scope is disposed. This matches .NET's scope disposal semantics.\n *\n * Supports the `using` keyword via `Symbol.dispose` and `Symbol.asyncDispose`:\n *\n * @example Synchronous disposal\n * ```ts\n * using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n * // db is automatically disposed when the block exits\n * ```\n *\n * @example Asynchronous disposal\n * ```ts\n * await using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n * // db is asynchronously disposed when the block exits\n * ```\n *\n * @example Manual disposal\n * ```ts\n * const scope = provider.createScope();\n * try {\n * const db = scope.serviceProvider.get(IDbContext);\n * // use db...\n * } finally {\n * await scope.asyncDispose();\n * }\n * ```\n *\n * @see {@link ServiceProvider.createScope}\n * @see {@link ServiceLifetime.Scoped}\n */\nexport class ServiceScope implements Disposable, AsyncDisposable {\n readonly #descriptors: Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n >;\n readonly #singletonCache: Map<SchemaBuilder<any, any, any, any, any>, any>;\n readonly #scopedCache: Map<SchemaBuilder<any, any, any, any, any>, any> =\n new Map();\n readonly #disposables: any[] = [];\n #disposed = false;\n\n /**\n * A child {@link IServiceProvider} that resolves services within this scope.\n *\n * - **Singleton** services are resolved from the root provider's cache.\n * - **Scoped** services are resolved from this scope's cache (created on\n * first access, reused within the scope).\n * - **Transient** services create a new instance on every call.\n *\n * @example\n * ```ts\n * const scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext); // scoped\n * const logger = scope.serviceProvider.get(ILogger); // singleton from root\n * ```\n */\n public readonly serviceProvider: ScopedServiceProvider;\n\n /**\n * @hidden\n */\n constructor(\n descriptors: Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n >,\n singletonCache: Map<SchemaBuilder<any, any, any, any, any>, any>\n ) {\n this.#descriptors = descriptors;\n this.#singletonCache = singletonCache;\n this.serviceProvider = new ScopedServiceProvider(this);\n }\n\n /**\n * Synchronously disposes all scoped services that implement\n * `Symbol.dispose`, in reverse creation order (LIFO).\n *\n * Services that only implement `Symbol.asyncDispose` are skipped during\n * synchronous disposal. Use {@link asyncDispose} to properly dispose all\n * services including async ones.\n *\n * @throws Re-throws the first error encountered during disposal, but\n * still attempts to dispose remaining services.\n */\n public dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n\n let firstError: unknown;\n\n // Dispose in reverse order (LIFO)\n for (let i = this.#disposables.length - 1; i >= 0; i--) {\n const instance = this.#disposables[i];\n try {\n if (isDisposable(instance)) {\n instance[Symbol.dispose]();\n }\n // async-only disposables are skipped during sync disposal —\n // use asyncDispose() to properly clean up async resources.\n } catch (err) {\n if (!firstError) firstError = err;\n }\n }\n\n this.#scopedCache.clear();\n\n if (firstError) throw firstError;\n }\n\n /**\n * Asynchronously disposes all scoped services that implement\n * `Symbol.asyncDispose` or `Symbol.dispose`, in reverse creation order\n * (LIFO).\n *\n * For each service:\n * - If it implements `Symbol.asyncDispose`, that method is awaited.\n * - Otherwise, if it implements `Symbol.dispose`, that method is called\n * synchronously.\n *\n * @throws Re-throws the first error encountered during disposal, but\n * still attempts to dispose remaining services.\n */\n public async asyncDispose(): Promise<void> {\n if (this.#disposed) return;\n this.#disposed = true;\n\n let firstError: unknown;\n\n // Dispose in reverse order (LIFO)\n for (let i = this.#disposables.length - 1; i >= 0; i--) {\n const instance = this.#disposables[i];\n try {\n if (isAsyncDisposable(instance)) {\n await instance[Symbol.asyncDispose]();\n } else if (isDisposable(instance)) {\n instance[Symbol.dispose]();\n }\n } catch (err) {\n if (!firstError) firstError = err;\n }\n }\n\n this.#scopedCache.clear();\n\n if (firstError) throw firstError;\n }\n\n /**\n * Implements the synchronous `Disposable` protocol. Delegates to\n * {@link dispose}.\n *\n * @example\n * ```ts\n * using scope = provider.createScope();\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Implements the asynchronous `AsyncDisposable` protocol. Delegates to\n * {@link asyncDispose}.\n *\n * @example\n * ```ts\n * await using scope = provider.createScope();\n * ```\n */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.asyncDispose();\n }\n\n /**\n * @hidden\n */\n getDescriptors(): Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n > {\n return this.#descriptors;\n }\n\n /**\n * @hidden\n */\n getSingletonCache(): Map<SchemaBuilder<any, any, any, any, any>, any> {\n return this.#singletonCache;\n }\n\n /**\n * @hidden\n */\n getScopedCache(): Map<SchemaBuilder<any, any, any, any, any>, any> {\n return this.#scopedCache;\n }\n\n /**\n * @hidden\n */\n trackDisposable(instance: any): void {\n if (isDisposable(instance) || isAsyncDisposable(instance)) {\n this.#disposables.push(instance);\n }\n }\n}\n\n/**\n * A service provider scoped to a {@link ServiceScope}. Resolves scoped\n * services from the scope's cache and singletons from the root cache.\n *\n * This class is not instantiated directly — obtain it via\n * `scope.serviceProvider`.\n *\n * @see {@link ServiceScope}\n */\nexport class ScopedServiceProvider implements IServiceProvider {\n readonly #scope: ServiceScope;\n\n /**\n * @hidden\n */\n constructor(scope: ServiceScope) {\n this.#scope = scope;\n }\n\n /**\n * Resolves a service within this scope.\n *\n * - **Singleton** services are resolved from the root provider's cache.\n * - **Scoped** services are created once per scope and cached.\n * - **Transient** services are created fresh on every call.\n *\n * @param schema - The schema instance used as the service identifier.\n * @returns The resolved service, typed as `InferType<typeof schema>`.\n * @throws {Error} If the schema is not registered.\n * @throws {Error} If a circular dependency is detected.\n *\n * @example\n * ```ts\n * using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n * ```\n */\n public get<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> {\n return this.#resolve(schema, new Set());\n }\n\n /**\n * Resolves a service within this scope, or returns `undefined` if not\n * registered.\n *\n * @param schema - The schema instance used as the service identifier.\n * @returns The resolved service or `undefined`.\n */\n public getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> | undefined {\n if (!this.#scope.getDescriptors().has(schema)) {\n return undefined;\n }\n return this.#resolve(schema, new Set());\n }\n\n /**\n * Resolves the dependencies described by a {@link FunctionSchemaBuilder}\n * and calls `implementation` with the resolved values. Scoped services\n * are resolved within this scope.\n *\n * @param funcSchema - A {@link FunctionSchemaBuilder} whose parameters\n * describe the services to inject.\n * @param implementation - A function whose parameters match the\n * `funcSchema` parameter types.\n * @returns The return value of `implementation`.\n *\n * @example\n * ```ts\n * using scope = provider.createScope();\n * const result = scope.serviceProvider.invoke(handler, (logger, db) => {\n * logger.info('Handling request');\n * return db.query('SELECT 1');\n * });\n * ```\n *\n * @see {@link ServiceProvider.invoke}\n */\n public invoke<\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n funcSchema: TFuncSchema,\n implementation: InferType<TFuncSchema>\n ): ReturnType<InferType<TFuncSchema>> {\n const parameterSchemas = funcSchema.introspect().parameters;\n const resolutionStack = new Set<\n SchemaBuilder<any, any, any, any, any>\n >();\n const args = parameterSchemas.map(paramSchema =>\n this.#resolve(paramSchema, resolutionStack)\n );\n return (implementation as Function).apply(null, args);\n }\n\n #resolve(\n schema: SchemaBuilder<any, any, any, any, any>,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>\n ): any {\n const descriptors = this.#scope.getDescriptors();\n const descriptor = descriptors.get(schema);\n if (!descriptor) {\n throw new Error(\n `Service not registered. The schema passed to get() was not ` +\n `registered in the ServiceCollection. Ensure you are ` +\n `using the same schema reference for registration and ` +\n `resolution.`\n );\n }\n\n // Circular dependency detection\n if (resolutionStack.has(schema)) {\n const chain = [...resolutionStack]\n .map(s => s.introspect().type ?? '(anonymous)')\n .join(' → ');\n throw new Error(\n `Circular dependency detected: ${chain} → ${descriptor.schema.introspect().type ?? '(anonymous)'}`\n );\n }\n\n switch (descriptor.lifetime) {\n case ServiceLifetime.Singleton: {\n const singletonCache = this.#scope.getSingletonCache();\n if (singletonCache.has(schema)) {\n return singletonCache.get(schema);\n }\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack\n );\n resolutionStack.delete(schema);\n singletonCache.set(schema, instance);\n return instance;\n }\n\n case ServiceLifetime.Scoped: {\n const scopedCache = this.#scope.getScopedCache();\n if (scopedCache.has(schema)) {\n return scopedCache.get(schema);\n }\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack\n );\n resolutionStack.delete(schema);\n scopedCache.set(schema, instance);\n this.#scope.trackDisposable(instance);\n return instance;\n }\n\n case ServiceLifetime.Transient: {\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack\n );\n resolutionStack.delete(schema);\n return instance;\n }\n }\n }\n\n /**\n * Internal resolution entry point that reuses an existing resolution\n * stack. Called by {@link ScopedResolverProxy} to propagate the stack\n * across factory-to-factory calls, enabling circular dependency\n * detection across the full chain.\n *\n * @hidden\n */\n resolveWithStack(\n schema: SchemaBuilder<any, any, any, any, any>,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>\n ): any {\n return this.#resolve(schema, resolutionStack);\n }\n\n #createAndValidate(\n descriptor: ServiceDescriptor,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>\n ): any {\n const childProxy = new ScopedResolverProxy(this, resolutionStack);\n const instance = descriptor.factory(childProxy);\n\n if (descriptor.validate) {\n const result = descriptor.schema.validate(instance);\n if (!result.valid) {\n const messages =\n result.errors?.map(e => e.message).join('; ') ??\n 'unknown error';\n throw new Error(\n `Service validation failed for schema type ` +\n `\"${descriptor.schema.introspect().type ?? '(anonymous)'}\": ${messages}`\n );\n }\n }\n\n return instance;\n }\n}\n\n/**\n * Internal proxy used during factory invocation within a scope.\n * Routes resolution back through the ScopedServiceProvider with the current\n * resolution stack for circular dependency detection.\n *\n * @hidden\n */\nclass ScopedResolverProxy implements IServiceProvider {\n readonly #provider: ScopedServiceProvider;\n readonly #resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>;\n\n constructor(\n provider: ScopedServiceProvider,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>\n ) {\n this.#provider = provider;\n this.#resolutionStack = resolutionStack;\n }\n\n get<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> {\n return this.#provider.resolveWithStack(schema, this.#resolutionStack);\n }\n\n getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> | undefined {\n return this.#provider.getOptional(schema);\n }\n}\n","import type {\n FunctionSchemaBuilder,\n InferType,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type { ServiceProviderOptions } from './ServiceCollection.js';\nimport { ServiceScope } from './ServiceScope.js';\nimport type { IServiceProvider, ServiceDescriptor } from './types.js';\nimport { ServiceLifetime } from './types.js';\n\n/**\n * An immutable service provider that resolves services from a frozen set of\n * {@link ServiceDescriptor | descriptors}. Obtain an instance by calling\n * {@link ServiceCollection.buildServiceProvider}.\n *\n * Supports three service lifetimes:\n * - **Singleton** — one instance per provider, cached after first resolution.\n * - **Scoped** — one instance per {@link ServiceScope}, created via\n * {@link createScope}.\n * - **Transient** — a new instance on every call to {@link get}.\n *\n * @example Resolving services\n * ```ts\n * const provider = services.buildServiceProvider();\n *\n * // Typed via the schema — no explicit generic needed\n * const config = provider.get(IConfig);\n * config.port; // number\n *\n * // Optional resolution (returns undefined if not registered)\n * const maybeMail = provider.getOptional(IMailer);\n * ```\n *\n * @example Using scopes\n * ```ts\n * // Per-request scope (e.g. in an HTTP handler)\n * using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n * // db is disposed when scope exits\n * ```\n *\n * @example Function injection\n * ```ts\n * const handlerSchema = func()\n * .addParameter(ILogger)\n * .addParameter(IDbContext)\n * .hasReturnType(string());\n *\n * const result = provider.invoke(handlerSchema, (logger, db) => {\n * logger.info('Handling request');\n * return db.query('SELECT 1');\n * });\n * ```\n *\n * @see {@link ServiceCollection}\n * @see {@link ServiceScope}\n */\nexport class ServiceProvider implements IServiceProvider {\n readonly #descriptors: Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n >;\n readonly #singletonCache: Map<SchemaBuilder<any, any, any, any, any>, any> =\n new Map();\n readonly #validateScopes: boolean;\n\n /**\n * @hidden\n */\n constructor(\n descriptors: Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n >,\n options?: ServiceProviderOptions\n ) {\n this.#descriptors = descriptors;\n this.#validateScopes = options?.validateScopes ?? true;\n }\n\n /**\n * Resolves a service by its schema key.\n *\n * - **Singleton**: returns the cached instance, or creates and caches it.\n * - **Scoped**: throws if called from the root provider when\n * `validateScopes` is `true` (default). Use\n * {@link createScope} to obtain a scoped provider.\n * - **Transient**: creates a new instance on every call.\n *\n * @param schema - The schema instance used as the service identifier.\n * Must be the same reference used during registration.\n * @returns The resolved service, typed as `InferType<typeof schema>`.\n * @throws {Error} If the schema is not registered.\n * @throws {Error} If a scoped service is resolved from the root provider\n * with `validateScopes` enabled.\n * @throws {Error} If a circular dependency is detected.\n *\n * @example\n * ```ts\n * const logger = provider.get(ILogger);\n * logger.info('Hello'); // fully typed\n * ```\n */\n public get<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> {\n return this.#resolve(schema, new Set());\n }\n\n /**\n * Resolves a service by its schema key, or returns `undefined` if the\n * schema is not registered.\n *\n * Unlike {@link get}, this method does **not** throw for unregistered\n * schemas. All other behaviour (lifetime, scope validation, circular\n * dependency detection) is the same.\n *\n * @param schema - The schema instance used as the service identifier.\n * @returns The resolved service or `undefined`.\n *\n * @example\n * ```ts\n * const mailer = provider.getOptional(IMailer);\n * if (mailer) {\n * mailer.send('test@example.com', 'Hello');\n * }\n * ```\n */\n public getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> | undefined {\n if (!this.#descriptors.has(schema)) {\n return undefined;\n }\n return this.#resolve(schema, new Set());\n }\n\n /**\n * Creates a new {@link ServiceScope}. Scoped services resolved from the\n * scope's {@link ServiceScope.serviceProvider | serviceProvider} are\n * cached per-scope and disposed when the scope is disposed.\n *\n * The returned scope implements both `Symbol.dispose` and\n * `Symbol.asyncDispose`, so it works with the `using` keyword:\n *\n * @returns A new {@link ServiceScope}.\n *\n * @example\n * ```ts\n * // Sync disposal\n * using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n *\n * // Async disposal\n * await using scope = provider.createScope();\n * const db = scope.serviceProvider.get(IDbContext);\n * ```\n */\n public createScope(): ServiceScope {\n return new ServiceScope(this.#descriptors, this.#singletonCache);\n }\n\n /**\n * Resolves the dependencies described by a {@link FunctionSchemaBuilder}\n * and calls `implementation` with the resolved values.\n *\n * Each parameter schema in `funcSchema.introspect().parameters` is\n * resolved from this provider. The implementation receives the resolved\n * values in the same order as the parameter schemas.\n *\n * @param funcSchema - A {@link FunctionSchemaBuilder} whose parameters\n * describe the services to inject.\n * @param implementation - A function whose parameters match the\n * `funcSchema` parameter types and whose return type matches the\n * `funcSchema` return type.\n * @returns The return value of `implementation`.\n *\n * @example\n * ```ts\n * const handler = func()\n * .addParameter(ILogger)\n * .addParameter(IConfig)\n * .hasReturnType(string());\n *\n * const result = provider.invoke(handler, (logger, config) => {\n * logger.info(`Running on port ${config.port}`);\n * return 'ok';\n * });\n * // result: string\n * ```\n *\n * @see {@link FunctionSchemaBuilder.addParameter}\n * @see {@link FunctionSchemaBuilder.hasReturnType}\n */\n public invoke<\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n funcSchema: TFuncSchema,\n implementation: InferType<TFuncSchema>\n ): ReturnType<InferType<TFuncSchema>> {\n const parameterSchemas = funcSchema.introspect().parameters;\n const resolutionStack = new Set<\n SchemaBuilder<any, any, any, any, any>\n >();\n const args = parameterSchemas.map(paramSchema =>\n this.#resolve(paramSchema, resolutionStack)\n );\n return (implementation as Function).apply(null, args);\n }\n\n /**\n * Internal resolution method shared between root and scoped providers.\n * Handles singleton caching and transient creation. Scoped resolution is\n * handled by {@link ScopedServiceProvider}.\n *\n * @hidden\n */\n #resolve(\n schema: SchemaBuilder<any, any, any, any, any>,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>,\n scopedCache?: Map<SchemaBuilder<any, any, any, any, any>, any>,\n trackDisposable?: (instance: any) => void\n ): any {\n const descriptor = this.#descriptors.get(schema);\n if (!descriptor) {\n throw new Error(\n `Service not registered. The schema passed to get() was not ` +\n `registered in the ServiceCollection. Ensure you are ` +\n `using the same schema reference for registration and ` +\n `resolution.`\n );\n }\n\n // Circular dependency detection\n if (resolutionStack.has(schema)) {\n const chain = [...resolutionStack]\n .map(s => s.introspect().type ?? '(anonymous)')\n .join(' → ');\n throw new Error(\n `Circular dependency detected: ${chain} → ${descriptor.schema.introspect().type ?? '(anonymous)'}`\n );\n }\n\n switch (descriptor.lifetime) {\n case ServiceLifetime.Singleton: {\n if (this.#singletonCache.has(schema)) {\n return this.#singletonCache.get(schema);\n }\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n resolutionStack.delete(schema);\n this.#singletonCache.set(schema, instance);\n return instance;\n }\n\n case ServiceLifetime.Scoped: {\n if (!scopedCache) {\n if (this.#validateScopes) {\n throw new Error(\n `Cannot resolve scoped service from the root ` +\n `provider. Create a scope first with ` +\n `provider.createScope(). If this is ` +\n `intentional, set validateScopes: false in ` +\n `buildServiceProvider() options.`\n );\n }\n // If validateScopes is false, treat as transient from root\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n resolutionStack.delete(schema);\n return instance;\n }\n if (scopedCache.has(schema)) {\n return scopedCache.get(schema);\n }\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n resolutionStack.delete(schema);\n scopedCache.set(schema, instance);\n if (trackDisposable) {\n trackDisposable(instance);\n }\n return instance;\n }\n\n case ServiceLifetime.Transient: {\n resolutionStack.add(schema);\n const instance = this.#createAndValidate(\n descriptor,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n resolutionStack.delete(schema);\n return instance;\n }\n }\n }\n\n #createAndValidate(\n descriptor: ServiceDescriptor,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>,\n scopedCache?: Map<SchemaBuilder<any, any, any, any, any>, any>,\n trackDisposable?: (instance: any) => void\n ): any {\n const childProvider = new ScopedResolverProxy(\n this,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n const instance = descriptor.factory(childProvider);\n\n if (descriptor.validate) {\n const result = descriptor.schema.validate(instance);\n if (!result.valid) {\n const messages =\n result.errors?.map(e => e.message).join('; ') ??\n 'unknown error';\n throw new Error(\n `Service validation failed for schema type ` +\n `\"${descriptor.schema.introspect().type ?? '(anonymous)'}\": ${messages}`\n );\n }\n }\n\n return instance;\n }\n\n /**\n * Resolves a service within a scope context. Called by\n * {@link ScopedServiceProvider}.\n *\n * @hidden\n */\n resolveScoped(\n schema: SchemaBuilder<any, any, any, any, any>,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>,\n scopedCache: Map<SchemaBuilder<any, any, any, any, any>, any>,\n trackDisposable: (instance: any) => void\n ): any {\n return this.#resolve(\n schema,\n resolutionStack,\n scopedCache,\n trackDisposable\n );\n }\n}\n\n/**\n * Internal proxy used during factory invocation to enable factories to resolve\n * other services. Routes resolution back through the ServiceProvider with the\n * current resolution stack for circular dependency detection.\n *\n * @hidden\n */\nclass ScopedResolverProxy implements IServiceProvider {\n readonly #provider: ServiceProvider;\n readonly #resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>;\n readonly #scopedCache?: Map<SchemaBuilder<any, any, any, any, any>, any>;\n readonly #trackDisposable?: (instance: any) => void;\n\n constructor(\n provider: ServiceProvider,\n resolutionStack: Set<SchemaBuilder<any, any, any, any, any>>,\n scopedCache?: Map<SchemaBuilder<any, any, any, any, any>, any>,\n trackDisposable?: (instance: any) => void\n ) {\n this.#provider = provider;\n this.#resolutionStack = resolutionStack;\n this.#scopedCache = scopedCache;\n this.#trackDisposable = trackDisposable;\n }\n\n get<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> {\n return this.#provider.resolveScoped(\n schema,\n this.#resolutionStack,\n this.#scopedCache ?? new Map(),\n this.#trackDisposable ?? (() => {})\n );\n }\n\n getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): InferType<TSchema> | undefined {\n try {\n return this.get(schema);\n } catch {\n return undefined;\n }\n }\n}\n","import type {\n FunctionSchemaBuilder,\n InferType,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport { ServiceProvider } from './ServiceProvider.js';\nimport type {\n ServiceDescriptor,\n ServiceFactory,\n ServiceRegistrationOptions\n} from './types.js';\nimport { ServiceLifetime } from './types.js';\n\n/**\n * Options for building a {@link ServiceProvider} from a {@link ServiceCollection}.\n *\n * @see {@link ServiceCollection.buildServiceProvider}\n */\nexport interface ServiceProviderOptions {\n /**\n * When `true`, resolving a {@link ServiceLifetime.Scoped | Scoped} service\n * from the root provider (outside of a scope) throws an error. This helps\n * catch accidental singleton captures of scoped services.\n *\n * Recommended for development. Matches .NET's `ValidateScopes` behaviour.\n *\n * @defaultValue `true`\n */\n validateScopes?: boolean;\n}\n\n/**\n * A mutable collection of service registrations. Build up the collection by\n * calling {@link addSingleton}, {@link addScoped}, or {@link addTransient},\n * then call {@link buildServiceProvider} to create an immutable\n * {@link ServiceProvider}.\n *\n * Schema instances act as service keys via reference equality — the same\n * schema object used during registration must be used during resolution.\n *\n * @example Basic registration and resolution\n * ```ts\n * import { ServiceCollection } from '@cleverbrush/di';\n * import { object, string, number, func } from '@cleverbrush/schema';\n *\n * const IConfig = object({ port: number(), host: string() });\n * const ILogger = object({ info: func() });\n *\n * const services = new ServiceCollection();\n * services.addSingleton(IConfig, { port: 3000, host: 'localhost' });\n * services.addSingleton(ILogger, () => ({ info: console.log }));\n *\n * const provider = services.buildServiceProvider();\n * const config = provider.get(IConfig); // { port: number; host: string }\n * ```\n *\n * @example Function-schema-driven registration\n * ```ts\n * const IGreeter = object({ greet: func() });\n * const greeterFactory = func()\n * .addParameter(IConfig)\n * .addParameter(ILogger);\n *\n * services.addSingletonFromSchema(\n * IGreeter,\n * greeterFactory,\n * (config, logger) => ({\n * greet() { logger.info(`Hello from ${config.host}`); }\n * })\n * );\n * ```\n *\n * @see {@link ServiceProvider}\n * @see {@link ServiceLifetime}\n */\nexport class ServiceCollection {\n readonly #descriptors: Map<\n SchemaBuilder<any, any, any, any, any>,\n ServiceDescriptor\n > = new Map();\n\n /**\n * Registers a service with {@link ServiceLifetime.Singleton | Singleton}\n * lifetime. The factory (or value) is called at most once; every\n * subsequent resolution returns the same instance.\n *\n * @param schema - The schema used as the service identifier.\n * @param factoryOrValue - Either a {@link ServiceFactory} function that\n * receives the provider, or a plain value (which is wrapped in a\n * factory automatically).\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @example Factory registration\n * ```ts\n * services.addSingleton(ILogger, (provider) => {\n * const config = provider.get(IConfig);\n * return new ConsoleLogger(config.logLevel);\n * });\n * ```\n *\n * @example Value shorthand\n * ```ts\n * services.addSingleton(IConfig, { port: 3000, host: 'localhost' });\n * ```\n *\n * @see {@link addScoped}\n * @see {@link addTransient}\n */\n public addSingleton<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema,\n factoryOrValue: ServiceFactory<InferType<TSchema>> | InferType<TSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#add(\n schema,\n ServiceLifetime.Singleton,\n factoryOrValue,\n options\n );\n }\n\n /**\n * Registers a service with {@link ServiceLifetime.Scoped | Scoped}\n * lifetime. One instance is created per {@link ServiceScope}; within a\n * scope every resolution returns the same instance.\n *\n * @param schema - The schema used as the service identifier.\n * @param factory - A {@link ServiceFactory} function that creates the\n * service instance. Unlike {@link addSingleton}, a plain value is\n * **not** accepted because scoped services must be freshly created\n * per scope.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @example\n * ```ts\n * const IDbContext = object({ query: func() });\n *\n * services.addScoped(IDbContext, (provider) => {\n * const config = provider.get(IConfig);\n * return new DbContext(config.connectionString);\n * });\n * ```\n *\n * @see {@link addSingleton}\n * @see {@link addTransient}\n */\n public addScoped<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema,\n factory: ServiceFactory<InferType<TSchema>>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#add(schema, ServiceLifetime.Scoped, factory, options);\n }\n\n /**\n * Registers a service with {@link ServiceLifetime.Transient | Transient}\n * lifetime. A new instance is created on every resolution.\n *\n * @param schema - The schema used as the service identifier.\n * @param factory - A {@link ServiceFactory} function that creates the\n * service instance.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @example\n * ```ts\n * const IRequestId = object({ id: string() });\n *\n * services.addTransient(IRequestId, () => ({\n * id: crypto.randomUUID()\n * }));\n * ```\n *\n * @see {@link addSingleton}\n * @see {@link addScoped}\n */\n public addTransient<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema,\n factory: ServiceFactory<InferType<TSchema>>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#add(schema, ServiceLifetime.Transient, factory, options);\n }\n\n /**\n * Registers a pre-created instance as a {@link ServiceLifetime.Singleton | Singleton}.\n * Unlike {@link addSingleton}, the second argument is always treated as the\n * service value — never as a factory function. This makes it safe to\n * register a function-typed service (e.g. a schema backed by `func()`)\n * without the container accidentally invoking it as a factory.\n *\n * @param schema - The schema used as the service identifier.\n * @param instance - The pre-created value to register. Can be any type\n * including a function.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @example Register a plain object instance\n * ```ts\n * const config = { port: 3000, host: 'localhost' };\n * services.addSingletonInstance(IConfig, config);\n * ```\n *\n * @example Register a function value (impossible with {@link addSingleton})\n * ```ts\n * const IHandler = func();\n * const myHandler = (req: Request) => new Response('ok');\n * services.addSingletonInstance(IHandler, myHandler);\n * ```\n *\n * @see {@link addSingleton}\n */\n public addSingletonInstance<\n TSchema extends SchemaBuilder<any, any, any, any, any>\n >(\n schema: TSchema,\n instance: InferType<TSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#addInstance(schema, instance, options);\n }\n\n /**\n * Registers a {@link ServiceLifetime.Singleton | Singleton} service whose\n * dependencies are described by a {@link FunctionSchemaBuilder}. The\n * container resolves each parameter schema from the registry and passes\n * the resolved values to `implementation`.\n *\n * @param targetSchema - The schema used as the service identifier for the\n * created service.\n * @param funcSchema - A {@link FunctionSchemaBuilder} whose\n * `introspect().parameters` list the dependency schemas.\n * @param implementation - A function whose parameters match the\n * `funcSchema` parameter schemas (in order) and returns the service\n * instance.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @example\n * ```ts\n * const IGreeter = object({ greet: func() });\n *\n * const greeterDeps = func()\n * .addParameter(IConfig)\n * .addParameter(ILogger);\n *\n * services.addSingletonFromSchema(\n * IGreeter,\n * greeterDeps,\n * (config, logger) => ({\n * greet() { logger.info(`Hello from ${config.host}`); }\n * })\n * );\n * ```\n *\n * @see {@link addScopedFromSchema}\n * @see {@link addTransientFromSchema}\n */\n public addSingletonFromSchema<\n TTargetSchema extends SchemaBuilder<any, any, any, any, any>,\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n targetSchema: TTargetSchema,\n funcSchema: TFuncSchema,\n implementation: (\n ...args: FuncSchemaParameters<TFuncSchema>\n ) => InferType<TTargetSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#addFromSchema(\n targetSchema,\n funcSchema,\n implementation,\n ServiceLifetime.Singleton,\n options\n );\n }\n\n /**\n * Registers a {@link ServiceLifetime.Scoped | Scoped} service whose\n * dependencies are described by a {@link FunctionSchemaBuilder}.\n *\n * @param targetSchema - The schema used as the service identifier.\n * @param funcSchema - A {@link FunctionSchemaBuilder} describing the\n * dependency schemas via its parameters.\n * @param implementation - A function receiving the resolved dependencies\n * and returning the service instance.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @see {@link addSingletonFromSchema}\n * @see {@link addTransientFromSchema}\n */\n public addScopedFromSchema<\n TTargetSchema extends SchemaBuilder<any, any, any, any, any>,\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n targetSchema: TTargetSchema,\n funcSchema: TFuncSchema,\n implementation: (\n ...args: FuncSchemaParameters<TFuncSchema>\n ) => InferType<TTargetSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#addFromSchema(\n targetSchema,\n funcSchema,\n implementation,\n ServiceLifetime.Scoped,\n options\n );\n }\n\n /**\n * Registers a {@link ServiceLifetime.Transient | Transient} service whose\n * dependencies are described by a {@link FunctionSchemaBuilder}.\n *\n * @param targetSchema - The schema used as the service identifier.\n * @param funcSchema - A {@link FunctionSchemaBuilder} describing the\n * dependency schemas via its parameters.\n * @param implementation - A function receiving the resolved dependencies\n * and returning the service instance.\n * @param options - Optional {@link ServiceRegistrationOptions}.\n * @returns `this` for chaining.\n *\n * @see {@link addSingletonFromSchema}\n * @see {@link addScopedFromSchema}\n */\n public addTransientFromSchema<\n TTargetSchema extends SchemaBuilder<any, any, any, any, any>,\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n targetSchema: TTargetSchema,\n funcSchema: TFuncSchema,\n implementation: (\n ...args: FuncSchemaParameters<TFuncSchema>\n ) => InferType<TTargetSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n return this.#addFromSchema(\n targetSchema,\n funcSchema,\n implementation,\n ServiceLifetime.Transient,\n options\n );\n }\n\n /**\n * Creates a new {@link ServiceProvider} from the registrations currently\n * contained in this collection.\n *\n * @param options - Optional {@link ServiceProviderOptions} to control\n * provider behaviour (e.g. scope validation).\n * @returns A new {@link ServiceProvider} instance.\n *\n * @example\n * ```ts\n * const provider = services.buildServiceProvider();\n * const logger = provider.get(ILogger);\n * ```\n *\n * @example With scope validation disabled\n * ```ts\n * const provider = services.buildServiceProvider({\n * validateScopes: false\n * });\n * ```\n */\n public buildServiceProvider(\n options?: ServiceProviderOptions\n ): ServiceProvider {\n const descriptors = new Map(this.#descriptors);\n return new ServiceProvider(descriptors, options);\n }\n\n #add<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema,\n lifetime: ServiceLifetime,\n factoryOrValue: ServiceFactory<InferType<TSchema>> | InferType<TSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n const factory: ServiceFactory<InferType<TSchema>> =\n typeof factoryOrValue === 'function'\n ? (factoryOrValue as ServiceFactory<InferType<TSchema>>)\n : () => factoryOrValue;\n\n this.#descriptors.set(schema, {\n schema,\n lifetime,\n factory,\n validate: options?.validate ?? false\n });\n\n return this;\n }\n\n #addInstance<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema,\n instance: InferType<TSchema>,\n options?: ServiceRegistrationOptions\n ): this {\n const factory: ServiceFactory<InferType<TSchema>> = () => instance;\n\n this.#descriptors.set(schema, {\n schema,\n lifetime: ServiceLifetime.Singleton,\n factory,\n validate: options?.validate ?? false\n });\n\n return this;\n }\n\n #addFromSchema<\n TTargetSchema extends SchemaBuilder<any, any, any, any, any>,\n TFuncSchema extends FunctionSchemaBuilder<any, any, any, any, any, any>\n >(\n targetSchema: TTargetSchema,\n funcSchema: TFuncSchema,\n implementation: (\n ...args: FuncSchemaParameters<TFuncSchema>\n ) => InferType<TTargetSchema>,\n lifetime: ServiceLifetime,\n options?: ServiceRegistrationOptions\n ): this {\n const parameterSchemas = funcSchema.introspect().parameters;\n\n const factory: ServiceFactory<InferType<TTargetSchema>> = provider => {\n const args = parameterSchemas.map(paramSchema =>\n provider.get(paramSchema)\n );\n return (implementation as Function).apply(null, args);\n };\n\n this.#descriptors.set(targetSchema, {\n schema: targetSchema,\n lifetime,\n factory,\n validate: options?.validate ?? false\n });\n\n return this;\n }\n}\n\n/**\n * Extracts the parameter types from a {@link FunctionSchemaBuilder} as a tuple.\n * Used internally to type-check `addSingletonFromSchema`-style registrations.\n */\ntype FuncSchemaParameters<\n T extends FunctionSchemaBuilder<any, any, any, any, any, any>\n> =\n T extends FunctionSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n infer TParams extends SchemaBuilder<any, any, any, any, any>[]\n >\n ? { [K in keyof TParams]: InferType<TParams[K]> }\n : never;\n"],"mappings":"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","names":["ServiceLifetime","isDisposable","value","isAsyncDisposable","ServiceScope","#descriptors","#singletonCache","#scopedCache","#disposables","#disposed","descriptors","singletonCache","ScopedServiceProvider","firstError","i","instance","isDisposable","err","isAsyncDisposable","#scope","scope","schema","#resolve","funcSchema","implementation","parameterSchemas","resolutionStack","args","paramSchema","descriptor","chain","s","#createAndValidate","scopedCache","childProxy","ScopedResolverProxy","result","messages","e","#provider","#resolutionStack","provider","ServiceProvider","#descriptors","#singletonCache","#validateScopes","descriptors","options","schema","#resolve","ServiceScope","funcSchema","implementation","parameterSchemas","resolutionStack","args","paramSchema","scopedCache","trackDisposable","descriptor","chain","s","instance","#createAndValidate","childProvider","ScopedResolverProxy","result","messages","e","#provider","#resolutionStack","#scopedCache","#trackDisposable","provider","ServiceCollection","#descriptors","schema","factoryOrValue","options","#add","factory","instance","#addInstance","targetSchema","funcSchema","implementation","#addFromSchema","descriptors","ServiceProvider","lifetime","parameterSchemas","provider","args","paramSchema"]}
@@ -0,0 +1,156 @@
1
+ import type { InferType, SchemaBuilder } from '@cleverbrush/schema';
2
+ /**
3
+ * Defines the lifetime of a service within the dependency injection container.
4
+ *
5
+ * The lifetime determines when a new instance is created versus when a cached
6
+ * instance is returned.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { ServiceCollection, ServiceLifetime } from '@cleverbrush/di';
11
+ * import { object, string } from '@cleverbrush/schema';
12
+ *
13
+ * const ILogger = object({ info: func() });
14
+ *
15
+ * const services = new ServiceCollection();
16
+ * // Equivalent to services.addSingleton(ILogger, ...)
17
+ * services.add(ILogger, () => console, ServiceLifetime.Singleton);
18
+ * ```
19
+ *
20
+ * @see {@link ServiceCollection}
21
+ */
22
+ export declare enum ServiceLifetime {
23
+ /**
24
+ * A new instance is created every time the service is resolved.
25
+ *
26
+ * Use for lightweight, stateless services.
27
+ */
28
+ Transient = "Transient",
29
+ /**
30
+ * One instance is created per {@link ServiceScope}. The same instance is
31
+ * returned for every resolution within that scope.
32
+ *
33
+ * Use for services that should be shared within a single unit of work
34
+ * (e.g. an HTTP request) but isolated between units.
35
+ */
36
+ Scoped = "Scoped",
37
+ /**
38
+ * One instance is created for the entire lifetime of the
39
+ * {@link ServiceProvider}. All subsequent resolutions return the same instance.
40
+ *
41
+ * Use for expensive-to-create or truly global services (loggers,
42
+ * configuration, connection pools).
43
+ */
44
+ Singleton = "Singleton"
45
+ }
46
+ /**
47
+ * A factory function that creates a service instance, optionally resolving
48
+ * other services from the provided {@link ServiceProvider}.
49
+ *
50
+ * @typeParam T - The type of the service instance the factory creates.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const loggerFactory: ServiceFactory<Logger> = (provider) => {
55
+ * const config = provider.get(IConfig);
56
+ * return new Logger(config.logLevel);
57
+ * };
58
+ * ```
59
+ *
60
+ * @see {@link ServiceCollection}
61
+ */
62
+ export type ServiceFactory<T> = (provider: IServiceProvider) => T;
63
+ /**
64
+ * Describes a registered service: its schema key, lifetime, factory, and
65
+ * optional runtime validation flag.
66
+ *
67
+ * @typeParam T - The type of the service instance.
68
+ *
69
+ * @see {@link ServiceCollection}
70
+ * @see {@link ServiceLifetime}
71
+ */
72
+ export interface ServiceDescriptor<T = any> {
73
+ /** The schema used as the service identifier (reference equality). */
74
+ readonly schema: SchemaBuilder<T, any, any, any, any>;
75
+ /** The lifetime of the service. */
76
+ readonly lifetime: ServiceLifetime;
77
+ /** The factory function that creates the service instance. */
78
+ readonly factory: ServiceFactory<T>;
79
+ /**
80
+ * When `true`, the container validates the factory's return value against
81
+ * the schema at resolution time using `schema.validate()`.
82
+ *
83
+ * @defaultValue `false`
84
+ */
85
+ readonly validate: boolean;
86
+ }
87
+ /**
88
+ * Options for service registration.
89
+ *
90
+ * @see {@link ServiceCollection.addSingleton}
91
+ * @see {@link ServiceCollection.addScoped}
92
+ * @see {@link ServiceCollection.addTransient}
93
+ */
94
+ export interface ServiceRegistrationOptions {
95
+ /**
96
+ * When `true`, the resolved value is validated against the schema at
97
+ * resolution time. Useful for development/debugging. Has a performance
98
+ * cost proportional to the schema complexity.
99
+ *
100
+ * @defaultValue `false`
101
+ */
102
+ validate?: boolean;
103
+ }
104
+ /**
105
+ * Minimal interface for the service provider, used to avoid circular
106
+ * dependencies between modules.
107
+ *
108
+ * @see {@link ServiceProvider}
109
+ */
110
+ export interface IServiceProvider {
111
+ /**
112
+ * Resolves a service by its schema key.
113
+ * @param schema - The schema instance used as the service identifier.
114
+ * @throws If the service is not registered.
115
+ */
116
+ get<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema>;
117
+ /**
118
+ * Resolves a service by its schema key, or returns `undefined` if not registered.
119
+ * @param schema - The schema instance used as the service identifier.
120
+ */
121
+ getOptional<TSchema extends SchemaBuilder<any, any, any, any, any>>(schema: TSchema): InferType<TSchema> | undefined;
122
+ }
123
+ /**
124
+ * Checks whether a value implements the synchronous `Disposable` protocol
125
+ * (`Symbol.dispose`).
126
+ *
127
+ * @param value - The value to check.
128
+ * @returns `true` if `value` has a `Symbol.dispose` method.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const resource = {
133
+ * [Symbol.dispose]() { console.log('disposed'); }
134
+ * };
135
+ * isDisposable(resource); // true
136
+ * isDisposable({}); // false
137
+ * ```
138
+ */
139
+ export declare function isDisposable(value: unknown): value is Disposable;
140
+ /**
141
+ * Checks whether a value implements the asynchronous `Disposable` protocol
142
+ * (`Symbol.asyncDispose`).
143
+ *
144
+ * @param value - The value to check.
145
+ * @returns `true` if `value` has a `Symbol.asyncDispose` method.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const connection = {
150
+ * async [Symbol.asyncDispose]() { await db.close(); }
151
+ * };
152
+ * isAsyncDisposable(connection); // true
153
+ * isAsyncDisposable({}); // false
154
+ * ```
155
+ */
156
+ export declare function isAsyncDisposable(value: unknown): value is AsyncDisposable;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
3
+ "bugs": {
4
+ "url": "https://github.com/cleverbrush/framework/issues",
5
+ "email": "andrew_zol@cleverbrush.com"
6
+ },
7
+ "dependencies": {
8
+ "@cleverbrush/schema": "0.0.0-beta-20260410162047"
9
+ },
10
+ "description": ".NET-style dependency injection container for TypeScript — schema-driven service registration, three lifetimes, function injection",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "homepage": "https://docs.cleverbrush.com/di",
15
+ "keywords": [
16
+ "dependency injection",
17
+ "di",
18
+ "ioc",
19
+ "service container",
20
+ "typescript",
21
+ "schema",
22
+ "cleverbrush"
23
+ ],
24
+ "license": "BSD 3-Clause",
25
+ "main": "./dist/index.js",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "sideEffects": false,
33
+ "name": "@cleverbrush/di",
34
+ "readme": "https://github.com/cleverbrush/framework/tree/master/libs/di#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "github:cleverbrush/framework"
38
+ },
39
+ "scripts": {
40
+ "watch": "tsc --build tsconfig.build.json --watch",
41
+ "build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
42
+ "clean": "rm -rf dist tsconfig.build.tsbuildinfo"
43
+ },
44
+ "type": "module",
45
+ "types": "./dist/index.d.ts",
46
+ "version": "0.0.0-beta-20260410162047"
47
+ }