@codefast/di 0.3.14 → 0.3.15

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.3.15
4
+
5
+ ### Patch Changes
6
+
7
+ - [`8492085`](https://github.com/codefastlabs/codefast/commit/849208521571b18a3af1f36566c3111a5af01b7c) Thanks [@thevuong](https://github.com/thevuong)! - refactor(di): enhance token exports for improved usability
8
+
9
+ - [`4df6e65`](https://github.com/codefastlabs/codefast/commit/4df6e6579faf21c6dc7622eb424ad213b120dabb) Thanks [@thevuong](https://github.com/thevuong)! - chore(tsdown): remove bench exclusions and streamline configuration files
10
+
3
11
  ## 0.3.14
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
@@ -7,6 +7,8 @@ Type-safe, ESM-only dependency injection for modern TypeScript — built on TC39
7
7
  [![npm downloads](https://img.shields.io/npm/dm/@codefast/di.svg)](https://www.npmjs.com/package/@codefast/di)
8
8
  [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
+ ---
11
+
10
12
  ## Table of Contents
11
13
 
12
14
  - [Why @codefast/di](#why-codefastdi)
@@ -29,6 +31,7 @@ Type-safe, ESM-only dependency injection for modern TypeScript — built on TC39
29
31
  - [Container](#container)
30
32
  - [Resolution](#resolution)
31
33
  - [Async resolution](#async-resolution)
34
+ - [Container API surface](#container-api-surface)
32
35
  - [Rebinding and unbinding](#rebinding-and-unbinding)
33
36
  - [Child containers](#child-containers)
34
37
  - [Validation](#validation)
@@ -48,21 +51,19 @@ Type-safe, ESM-only dependency injection for modern TypeScript — built on TC39
48
51
  `@codefast/di` is a small IoC container designed for applications that compile to ESM and want strong typing without metadata reflection tricks.
49
52
 
50
53
  - **Typed tokens.** `Token<Value>` flows through every `bind → resolve` path; the return type of `resolve()` is the one you registered.
51
- - **Native Stage 3 decorators.** `@injectable`, `inject`, `optional`, `@postConstruct`, `@preDestroy` write to `Symbol.metadata`. No `reflect-metadata`, no `experimentalDecorators`.
52
- - **Fluent binding API.** Constants, classes, sync/async factories, aliases, named/tagged/predicate constraints, activation + deactivation hooks.
53
- - **Module system.** `Module` and `AsyncModule` bundle bindings into reusable units that can be loaded, unloaded, and re-used across containers.
54
- - **Scope safety.** Detects captive dependencies (a singleton depending on a scoped or transient binding) during development and test.
55
- - **Async-first.** Dedupes in-flight async singleton construction and supports `await using` for automatic cleanup.
54
+ - **Native Stage 3 decorators.** `@injectable`, `inject`, `optional`, `@postConstruct`, `@preDestroy` record metadata for the resolver. Implementations use a `WeakMap` and mirror into `Symbol.metadata` when the decorator runtime supplies it — no `reflect-metadata`, no `experimentalDecorators`.
55
+ - **Fluent binding API.** Constants, classes, sync/async factories, resolved factories, aliases, named/tagged/predicate constraints, activation + deactivation hooks.
56
+ - **Module system.** `Module` / `AsyncModule` bundle bindings into reusable units that can be loaded, unloaded, and re-used across containers.
57
+ - **Scope checks.** Call `validate()` to detect captive dependencies (for example a `singleton` depending on a `scoped` or `transient` binding).
58
+ - **Async resolution.** Dedupes in-flight async singleton construction and supports `await using` for automatic cleanup.
56
59
  - **Tree-shakeable subpaths.** Import only the surface you need.
57
60
 
58
61
  ---
59
62
 
60
63
  ## Requirements
61
64
 
62
- | Dependency | Version |
63
- | ---------- | ----------------------------- |
64
- | Node.js | `>= 22.0.0` |
65
- | TypeScript | `>= 5.2` (Stage 3 decorators) |
65
+ - Node.js `>= 22.0.0` (see `package.json` → `engines`)
66
+ - TypeScript `>= 5.2` with native Stage 3 decorators (TypeScript `5.9+` recommended for best inference, consistent with other Codefast packages)
66
67
 
67
68
  Enable native decorators in `tsconfig.json` — do **not** enable `experimentalDecorators`:
68
69
 
@@ -128,7 +129,7 @@ container.rebind(LoggerToken).toConstantValue({
128
129
  container.resolve(CheckoutService).complete("ORD-1002");
129
130
  ```
130
131
 
131
- `@injectable([...])` lists constructor dependencies in order. The key idea: business classes (`CheckoutService`) stay unchanged while behavior swaps by rebinding infrastructure (`LoggerToken`) per environment.
132
+ `@injectable([...])` lists constructor dependencies in parameter order. Business classes (`CheckoutService`) stay unchanged while infrastructure (`LoggerToken`) swaps per environment.
132
133
 
133
134
  ---
134
135
 
@@ -141,7 +142,7 @@ container.resolve(CheckoutService).complete("ORD-1002");
141
142
  | **Container** | Holds a `BindingRegistry` and a `ScopeManager`; resolves bindings through the `DependencyResolver`. Supports child containers and async disposal. |
142
143
  | **Scope** | Instance lifetime: `singleton` (one per root container, shared with children), `scoped` (one per child container), `transient` (new on every resolve). |
143
144
  | **Module** | Reusable bundle of bindings. Loaded once per container; loading the same module twice is a no-op. |
144
- | **Metadata** | `@injectable([...])` writes constructor descriptors into `Symbol.metadata`, which the container reads at resolution time. |
145
+ | **Metadata** | `@injectable([...])` stores constructor parameter descriptors so the resolver knows what to inject at each index. |
145
146
 
146
147
  ---
147
148
 
@@ -155,7 +156,7 @@ const CacheToken = token<Cache>("Cache");
155
156
  ```
156
157
 
157
158
  - The type parameter flows through the binding and resolution chain.
158
- - Tokens use reference equality; two tokens with the same name are two distinct keys.
159
+ - Tokens use reference equality; two tokens with the same name string are still two distinct keys.
159
160
  - A class constructor can itself be a key:
160
161
 
161
162
  ```typescript
@@ -167,19 +168,20 @@ container.resolve(UserService); // returns UserService
167
168
 
168
169
  ## Bindings
169
170
 
170
- Start with `container.bind(key)` and chain a strategy, then optionally a scope, constraints, and hooks.
171
+ Start with `container.bind(key)` and chain a strategy, then optional constraints and hooks. For strategies that support it, call `.singleton()`, `.scoped()`, or `.transient()` after constraints.
171
172
 
172
173
  ### Strategies
173
174
 
174
- | Method | Description |
175
- | ---------------------------- | --------------------------------------------------------------------------- |
176
- | `.toConstantValue(value)` | Bind to a fixed value. Always treated as a singleton. |
177
- | `.toSelf()` | Bind a constructor to itself. Reads `@injectable()` metadata. |
178
- | `.to(Constructor)` | Bind a token to a class constructor. |
179
- | `.toDynamic(factory)` | Bind to a sync factory `(ctx: ResolutionContext) => Value`. |
180
- | `.toDynamicAsync(factory)` | Bind to an async factory `(ctx: ResolutionContext) => Promise<Value>`. |
181
- | `.toResolved(factory, deps)` | Bind to a factory whose dependency array is checked against the deps types. |
182
- | `.toAlias(targetToken)` | Redirect resolution to another token; the alias adopts the target's scope. |
175
+ | Method | Description |
176
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
177
+ | `.toConstantValue(value)` | Fixed value. Stored as a `constant` binding with scope `singleton` (no `.scoped()` / `.transient()` chain). |
178
+ | `.toSelf()` | Bind a constructor to itself. Uses `@injectable()` metadata. |
179
+ | `.to(Constructor)` | Bind a token to a class constructor. |
180
+ | `.toDynamic(factory)` | Sync factory `(ctx: ResolutionContext) => Value`. |
181
+ | `.toDynamicAsync(factory)` | Async factory `(ctx: ResolutionContext) => Promise<Value>`. |
182
+ | `.toResolved(factory, deps)` | Factory with a typed dependency tuple; dependencies are resolved in order. |
183
+ | `.toResolvedAsync(factory, deps)` | Like `toResolved`, but the factory returns a `Promise`. |
184
+ | `.toAlias(targetToken)` | Redirect resolution to another token; the alias follows the target’s materialization. |
183
185
 
184
186
  ```typescript
185
187
  container.bind(AppConfigToken).toConstantValue({ port: 3000 });
@@ -203,6 +205,10 @@ container
203
205
  .bind(UserServiceToken)
204
206
  .toResolved((repo, cfg) => new UserService(repo, cfg), [UserRepository, AppConfigToken] as const);
205
207
 
208
+ container
209
+ .bind(MetricsToken)
210
+ .toResolvedAsync(async (db) => new MetricsCollector(db), [DbToken] as const);
211
+
206
212
  container.bind(LegacyServiceToken).toAlias(NewServiceToken);
207
213
  ```
208
214
 
@@ -214,6 +220,8 @@ container.bind(LegacyServiceToken).toAlias(NewServiceToken);
214
220
  | `.scoped()` | One instance per child container. Useful for request-scoped services. |
215
221
  | `.transient()` | New instance on every resolution. Default when no scope method is chained. |
216
222
 
223
+ `toConstantValue` does not expose scope chaining: constants are always treated as singletons internally.
224
+
217
225
  ```typescript
218
226
  container.bind(DatabaseToken).toDynamic(createDb).singleton();
219
227
  container.bind(RequestContextToken).toSelf().scoped();
@@ -242,6 +250,10 @@ container.bind(StorageToken).to(LocalStorage).whenTagged("provider", "local");
242
250
  container.resolve(StorageToken, { tag: ["provider", "s3"] });
243
251
  ```
244
252
 
253
+ **Default slot**
254
+
255
+ `.whenDefault()` is a documentation-only marker — it is a no-op at runtime. A binding without any constraint already participates in resolution when no `name` / `tag` hint is provided. Use it to signal intent when mixing constrained and unconstrained bindings for the same token.
256
+
245
257
  **Predicate** — inspect the full resolution graph:
246
258
 
247
259
  ```typescript
@@ -250,13 +262,18 @@ import { whenAnyAncestorIs, whenParentIs, whenParentTagged } from "@codefast/di/
250
262
  container.bind(LoggerToken).toConstantValue(verboseLogger).when(whenParentIs(DiagnosticsService));
251
263
  ```
252
264
 
253
- Built-in predicates:
265
+ Built-in predicates (all from `@codefast/di/constraints`):
254
266
 
255
- | Helper | Matches when … |
256
- | ------------------------------ | --------------------------------------------------------------------------- |
257
- | `whenParentIs(key)` | the direct parent binding was registered for `key`. |
258
- | `whenAnyAncestorIs(key)` | any ancestor binding on the materialization stack was registered for `key`. |
259
- | `whenParentTagged(tag, value)` | the immediate parent binding carries `tag` with `value`. |
267
+ | Helper | Matches when … |
268
+ | ----------------------------------- | -------------------------------------------------------------------------- |
269
+ | `whenParentIs(key)` | The direct parent binding was registered for `key`. |
270
+ | `whenNoParentIs(key)` | There is no parent, or the parent is not registered for `key`. |
271
+ | `whenAnyAncestorIs(key)` | Any ancestor on the materialization stack was registered for `key`. |
272
+ | `whenNoAncestorIs(key)` | No ancestor was registered for `key`. |
273
+ | `whenParentNamed(name)` | The immediate parent binding’s slot name is `name`. |
274
+ | `whenAnyAncestorNamed(name)` | Some ancestor’s slot name is `name`. |
275
+ | `whenParentTagged(tag, value)` | The immediate parent binding carries `tag` with `value` (via `Object.is`). |
276
+ | `whenAnyAncestorTagged(tag, value)` | Some ancestor carries `tag` with `value`. |
260
277
 
261
278
  For anything else, pass a custom `(ctx: ConstraintContext) => boolean` to `.when(predicate)`.
262
279
 
@@ -276,18 +293,18 @@ container
276
293
  });
277
294
  ```
278
295
 
279
- - `onActivation(ctx, instance)` runs after construction (and after `@postConstruct`). It must return the instance (possibly wrapped).
280
- - `onDeactivation(instance)` runs when the instance is evicted: `container.dispose()`, `unbind`, `rebind`, or `unload`.
296
+ - `onActivation(ctx, instance)` on the **binding** runs after `@postConstruct` and before **container-level** `onActivation` handlers registered with `container.onActivation(token, …)`. See `LifecycleManager.runActivation`.
297
+ - `onDeactivation(instance)` on the **binding** runs after **container-level** `onDeactivation` hooks and before `@preDestroy`. See `LifecycleManager.runDeactivation`.
281
298
 
282
299
  ---
283
300
 
284
301
  ## Decorators
285
302
 
286
- All decorators use TC39 Stage 3 syntax and write to `Symbol.metadata`.
303
+ All decorators use TC39 Stage 3 syntax. Metadata is stored for resolution as described in [Core Concepts](#core-concepts).
287
304
 
288
305
  ### `@injectable`
289
306
 
290
- Registers constructor dependencies. The array length must match the constructor arity; a mismatch throws `InternalError` at class-definition time.
307
+ Registers constructor dependencies in **parameter order** (index `0` → first constructor parameter). Each entry is either a `Token` / `Constructor` or an `InjectionDescriptor` from `inject()` / `optional()` / `injectAll()`.
291
308
 
292
309
  ```typescript
293
310
  import { inject, injectable, optional, token } from "@codefast/di";
@@ -304,7 +321,7 @@ class AppService {
304
321
  }
305
322
  ```
306
323
 
307
- Each entry is either a plain `Token` / `Constructor` or an `InjectionDescriptor` produced by `inject()` / `optional()` / `injectAll()`.
324
+ Keep the metadata array aligned with the constructor parameter list. Too few entries means `new` receives `undefined` for missing positions; too many adds unused metadata entries.
308
325
 
309
326
  ### `inject` / `optional` / `injectAll`
310
327
 
@@ -315,27 +332,30 @@ optional(CacheToken);
315
332
  injectAll(PluginToken);
316
333
  ```
317
334
 
318
- - `inject(token, options?)` — required. Throws `TokenNotBoundError` if the dependency is missing.
335
+ - `inject(token, options?)` — required. Throws `TokenNotBoundError` when the dependency cannot be resolved.
319
336
  - `optional(token, options?)` — optional. Resolves to `undefined` when unbound.
320
- - `injectAll(token, options?)` — resolves every matching binding into an array (`Value[]`), applying `name`/`tag` filters when provided.
337
+ - `injectAll(token, options?)` — resolves every matching binding into an array (`Value[]`), applying `name` / `tag` filters when provided.
321
338
 
322
339
  ### Accessor injection
323
340
 
324
341
  `inject` doubles as a TC39 accessor-field decorator for post-construction property injection:
325
342
 
326
343
  ```typescript
327
- @injectable([])
344
+ @injectable()
328
345
  class Controller {
329
346
  @inject(LoggerToken) accessor logger!: Logger;
330
347
  }
331
348
  ```
332
349
 
333
- The container injects the accessor after construction, so it does not count toward the constructor arity declared in `@injectable([])`.
350
+ The container injects the accessor after construction, so accessor fields do not use slots in the `@injectable([...])` arity list.
334
351
 
335
352
  ### `@postConstruct` / `@preDestroy`
336
353
 
337
- Method decorators that hook into the instance lifecycle. Order:
338
- `construct → @postConstruct → onActivation → cache … onDeactivation → @preDestroy`.
354
+ Method decorators that hook into the instance lifecycle.
355
+
356
+ **Activation** (after `new`): `@postConstruct` → binding `onActivation` → `container.onActivation(token)` hooks (in registration order).
357
+
358
+ **Deactivation** (on eviction): `container.onDeactivation(token)` hooks → binding `onDeactivation` → `@preDestroy`.
339
359
 
340
360
  ```typescript
341
361
  import { postConstruct, preDestroy } from "@codefast/di";
@@ -360,19 +380,24 @@ Only one of each decorator is allowed per class.
360
380
 
361
381
  ### Auto-registration
362
382
 
363
- Pass `autoRegister` to `@injectable` to have the class register itself in a module-scoped list:
383
+ Pass an `AutoRegisterRegistry` from `createAutoRegisterRegistry()` into `@injectable` options. Each decorated class registers itself with an optional `scope` (`"transient"` by default).
364
384
 
365
385
  ```typescript
366
- @injectable([DbToken], { autoRegister: true, scope: "singleton" })
386
+ import { Container, createAutoRegisterRegistry, injectable, token } from "@codefast/di";
387
+
388
+ const DbToken = token<Database>("Database");
389
+ const autoRegister = createAutoRegisterRegistry();
390
+
391
+ @injectable([DbToken], { autoRegister, scope: "singleton" })
367
392
  class UserRepository {
368
393
  constructor(private readonly db: Database) {}
369
394
  }
370
395
 
371
- // Bind every auto-registered class at once
372
- container.loadAutoRegistered();
396
+ const container = Container.create();
397
+ container.loadAutoRegistered(autoRegister);
373
398
  ```
374
399
 
375
- `scope` defaults to `"transient"`. `getAutoRegistered()` returns the list if you prefer to iterate manually.
400
+ To bind manually, iterate `autoRegister.entries()` and call `container.bind(entry.target).toSelf()` (and apply the desired scope).
376
401
 
377
402
  ---
378
403
 
@@ -392,55 +417,76 @@ container.resolve(StorageToken, { tag: ["provider", "s3"] });
392
417
 
393
418
  container.has(CacheToken);
394
419
  container.has(LoggerToken, { name: "console" });
420
+ container.hasOwn(LoggerToken, { name: "console" });
395
421
  ```
396
422
 
397
423
  `resolveAll()` and `ctx.resolveAll()` preserve the current resolution context (path + parent/ancestors stack), so `when(...)` predicates and scope checks behave the same as `resolve()`.
398
424
 
399
425
  ### Async resolution
400
426
 
401
- Use the `*Async` variants when any binding in the resolution chain uses `toDynamicAsync`, async `onActivation`, or async `@postConstruct`. Mixing async into a sync resolve throws `AsyncResolutionError`.
427
+ Use the `*Async` variants when any binding in the resolution chain uses `toDynamicAsync`, `toResolvedAsync`, async `onActivation`, or async `@postConstruct`. Mixing async into a sync resolve throws `AsyncResolutionError`.
402
428
 
403
429
  ```typescript
404
430
  const db = await container.resolveAsync(DbToken);
405
431
  const handlers = await container.resolveAllAsync(HandlerToken);
406
- // resolveOptional is sync-only — for async optional lookups, check `has` then `resolveAsync`
407
- const cache = container.has(CacheToken) ? await container.resolveAsync(CacheToken) : undefined;
432
+ const cache = await container.resolveOptionalAsync(CacheToken);
408
433
 
409
- // Eagerly construct every singleton binding
434
+ // Eagerly construct eligible singleton bindings
410
435
  await container.initializeAsync();
411
436
  ```
412
437
 
438
+ ### Container API surface
439
+
440
+ | Area | Methods / properties |
441
+ | ------------------ | ----------------------------------------------------------------------------------------------------- |
442
+ | **Lifecycle** | `dispose()`, `[Symbol.asyncDispose]()`, `[Symbol.dispose]()` (throws `SyncDisposalNotSupportedError`) |
443
+ | **Bindings** | `bind`, `rebind`, `unbind`, `unbindAsync`, `unbindAll`, `unbindAllAsync` |
444
+ | **Modules** | `load`, `loadAsync`, `unload`, `unloadAsync`, `loadAutoRegistered` |
445
+ | **Global hooks** | `onActivation`, `onDeactivation` |
446
+ | **Resolve** | `resolve`, `resolveAsync`, `resolveOptional`, `resolveOptionalAsync`, `resolveAll`, `resolveAllAsync` |
447
+ | **Scopes / graph** | `createChild`, `validate`, `initializeAsync` |
448
+ | **Introspection** | `has`, `hasOwn`, `lookupBindings`, `inspect`, `generateDependencyGraph` |
449
+ | **State** | `isDisposed` |
450
+
413
451
  ### Rebinding and unbinding
414
452
 
415
453
  ```typescript
416
454
  container.rebind(LoggerToken).toConstantValue(testLogger);
417
455
 
418
- container.unbind(CacheToken); // sync deactivation hooks only
456
+ container.unbind(CacheToken); // sync deactivation only
419
457
  await container.unbindAsync(CacheToken); // awaits async deactivation
458
+
459
+ container.unbindAll();
460
+ await container.unbindAllAsync();
420
461
  ```
421
462
 
422
463
  ### Child containers
423
464
 
424
- Child containers fall through to the parent's bindings and share the parent's singleton cache, but have their own scoped cache.
465
+ Child containers fall through to the parent’s bindings and share the parent’s singleton cache, but maintain their own scoped cache.
425
466
 
426
467
  ```typescript
427
468
  const requestContainer = container.createChild();
428
469
 
429
- requestContainer.bind(RequestContextToken).toConstantValue(req).scoped();
470
+ requestContainer
471
+ .bind(RequestContextToken)
472
+ .toDynamic(() => req)
473
+ .scoped();
430
474
  const service = requestContainer.resolve(RequestScopedService);
431
475
 
432
- await requestContainer.dispose(); // releases scoped instances
476
+ await requestContainer.dispose(); // releases scoped instances owned by this child
433
477
  ```
434
478
 
435
479
  ### Validation
436
480
 
437
- `validate()` statically checks that no singleton binding depends on a `scoped` or `transient` binding (a captive dependency). In development and test environments the container runs `validate()` at most once after registry changes, so most scope violations surface without an explicit call.
481
+ `validate()` walks singleton bindings and fails fast when a captive dependency is detected (for example `singleton` → `scoped` / `transient`).
438
482
 
439
483
  ```typescript
440
484
  container.validate(); // throws ScopeViolationError on the first violation
441
485
  ```
442
486
 
443
- Control the environment heuristic via `NODE_ENV` — see `isDevelopmentOrTestEnvironment` in `@codefast/di/environment`.
487
+ Call it after meaningful registry changes (or in tests) — the container does **not** auto-invoke `validate()` based on `NODE_ENV`.
488
+
489
+ > **Scope of checks.** Only `class`, `toResolved`, and `toResolvedAsync` bindings are inspected, because their dependency lists are statically declared. `toDynamic` and `toDynamicAsync` bindings are skipped — their factories have no declared deps and cannot be checked statically.
444
490
 
445
491
  ### Introspection
446
492
 
@@ -450,16 +496,14 @@ import { toDotGraph } from "@codefast/di/graph-adapters/dot";
450
496
  import { toReactFlowGraph } from "@codefast/di/graph-adapters/reactflow";
451
497
 
452
498
  const snapshot = container.inspect();
453
- const json = container.generateDependencyGraph({ hideInternals: true });
499
+ const json = container.generateDependencyGraph({ includeParent: true });
454
500
  const dot = toDotGraph(json);
455
501
 
456
- // Adapters are pure converters from the canonical JSON graph.
457
502
  const cytoscape = toCytoscapeGraph(json);
458
503
  const reactflow = toReactFlowGraph(json);
459
504
  ```
460
505
 
461
- `generateDependencyGraph` always returns the canonical typed `ContainerGraphJson` (`nodes` + `edges`).
462
- Keep visualization adapters (`toDotGraph`, `toCytoscapeGraph`, `toReactFlowGraph`, or your own converters) outside container/inspector core APIs and import them from direct subpaths under `@codefast/di/graph-adapters/*`.
506
+ `generateDependencyGraph` returns the canonical `ContainerGraphJson` (`nodes`, `edges`, `includesParent`). Adapters are pure converters; import them from `@codefast/di/graph-adapters/*`.
463
507
 
464
508
  ### Disposal
465
509
 
@@ -472,10 +516,10 @@ Keep visualization adapters (`toDotGraph`, `toCytoscapeGraph`, `toReactFlowGraph
472
516
 
473
517
  const db = await container.resolveAsync(DbToken);
474
518
  // …
475
- } // dispose() runs all deactivation hooks
519
+ } // dispose() runs deactivation hooks for owned singletons
476
520
  ```
477
521
 
478
- Sync `using` is intentionally rejected: calling `Symbol.dispose` throws. Use `await using` or `await container.dispose()`.
522
+ Synchronous `using` is rejected: `[Symbol.dispose]()` throws `SyncDisposalNotSupportedError`. Use `await using` or `await container.dispose()`.
479
523
 
480
524
  ---
481
525
 
@@ -483,10 +527,10 @@ Sync `using` is intentionally rejected: calling `Symbol.dispose` throws. Use `aw
483
527
 
484
528
  Modules bundle related bindings into reusable units. A module holds no runtime state and can be loaded into any number of containers.
485
529
 
486
- Use the same fluent order everywhere (including inside modules): `bind(token).to*(…).when*(…).scope()…`.
530
+ Use the same fluent order everywhere (including inside modules): `bind(token).to*(…).when*(…)` then, when supported, `.singleton()` / `.scoped()` / `.transient()`.
487
531
 
488
532
  - Register **multiple** implementations for one token with separate chains, e.g. `api.bind(T).to(A).whenNamed("a")` and `api.bind(T).to(B).whenNamed("b")`.
489
- - **Last-wins** applies per slot (default vs named vs tag-set), as in the main container API.
533
+ - **Last-wins** applies per slot (default vs named vs tag-set), matching the container API.
490
534
 
491
535
  ```typescript
492
536
  import { Container, Module } from "@codefast/di";
@@ -505,7 +549,9 @@ const AppModule = Module.create("App", (api) => {
505
549
  const container = Container.fromModules(AppModule);
506
550
  ```
507
551
 
508
- Async modules may `await` during setup (e.g. dynamic config):
552
+ `Module.create` returns a `SyncModule`. `Module.createAsync` returns an `AsyncModule` (same as `AsyncModule.create`). `SyncModule`, `AsyncModule`, and `isSyncModule()` are available from `@codefast/di/module`.
553
+
554
+ Async modules may `await` during setup (for example remote config):
509
555
 
510
556
  ```typescript
511
557
  const DbModule = Module.createAsync("Database", async (api) => {
@@ -526,7 +572,7 @@ container.unload(AppModule);
526
572
  await container.unloadAsync(DbModule);
527
573
  ```
528
574
 
529
- Re-loading a module already present is a no-op. Circular imports between modules throw `CircularDependencyError`.
575
+ Re-loading a module that is already loaded increments an internal ref-count and does not re-register bindings; unloading decrements it and only removes bindings when the count reaches zero. Circular imports between modules are silently deduplicated — `CircularDependencyError` is only thrown for cycles in the dependency resolution graph (e.g. service A depends on service B which depends on A).
530
576
 
531
577
  ---
532
578
 
@@ -534,19 +580,31 @@ Re-loading a module already present is a no-op. Circular imports between modules
534
580
 
535
581
  All errors extend `DiError` and expose a stable `code` property.
536
582
 
537
- | Error class | `code` | Thrown when |
538
- | ------------------------- | ----------------------- | ----------------------------------------------------------- |
539
- | `TokenNotBoundError` | `"TOKEN_NOT_BOUND"` | A required token has no binding |
540
- | `NoMatchingBindingError` | `"NO_MATCHING_BINDING"` | A name/tag/predicate hint matches no registered binding |
541
- | `CircularDependencyError` | `"CIRCULAR_DEPENDENCY"` | A cycle is detected in dependency or module resolution |
542
- | `MissingMetadataError` | `"MISSING_METADATA"` | A class binding is missing `@injectable()` metadata |
543
- | `AsyncModuleLoadError` | `"ASYNC_MODULE_LOAD"` | Sync `load()` is called with an `AsyncModule` |
544
- | `AsyncResolutionError` | `"ASYNC_RESOLUTION"` | An async binding is reached during a sync `resolve()` |
545
- | `ScopeViolationError` | `"SCOPE_VIOLATION"` | A singleton depends on a scoped or transient binding |
546
- | `InternalError` | `"INTERNAL"` | Invariant violations (should never surface in correct code) |
583
+ | Error class | `code` | Thrown when |
584
+ | ------------------------------- | ------------------------------- | ----------------------------------------------------------------------- |
585
+ | `AmbiguousBindingError` | `"AMBIGUOUS_BINDING"` | Multiple bindings matched without a single decisive constraint winner |
586
+ | `AsyncDeactivationError` | `"ASYNC_DEACTIVATION"` | Async `onDeactivation` reached through `unbind` / sync paths |
587
+ | `AsyncModuleLoadError` | `"ASYNC_MODULE_LOAD"` | Sync `load()` used with an `AsyncModule` |
588
+ | `AsyncResolutionError` | `"ASYNC_RESOLUTION"` | Async work required during a sync `resolve()` |
589
+ | `CircularDependencyError` | `"CIRCULAR_DEPENDENCY"` | Cycle in dependency or module graph |
590
+ | `DisposedContainerError` | `"DISPOSED_CONTAINER"` | Operation after `dispose()` |
591
+ | `InternalError` | `"INTERNAL_ERROR"` | Invariant violations (should not surface in correct consumer code) |
592
+ | `MissingContainerContextError` | `"MISSING_CONTAINER_CONTEXT"` | `@inject` accessor resolved without an active container |
593
+ | `MissingMetadataError` | `"MISSING_METADATA"` | Class resolution missing `@injectable()` metadata |
594
+ | `MissingScopeContextError` | `"MISSING_SCOPE_CONTEXT"` | `scoped` binding resolved without a child container context |
595
+ | `NoMatchingBindingError` | `"NO_MATCHING_BINDING"` | Hint matches no registered binding |
596
+ | `RebindUnboundTokenError` | `"REBIND_UNBOUND_TOKEN"` | `rebind` targets a token with no binding owned by this container |
597
+ | `ScopeViolationError` | `"SCOPE_VIOLATION"` | Captive dependency found by `validate()` (`details` describes the path) |
598
+ | `SyncDisposalNotSupportedError` | `"SYNC_DISPOSAL_NOT_SUPPORTED"` | Sync `using` / `[Symbol.dispose]` on the container |
599
+ | `TokenNotBoundError` | `"TOKEN_NOT_BOUND"` | Required token has no binding |
547
600
 
548
601
  ```typescript
549
- import { DiError, ScopeViolationError, TokenNotBoundError } from "@codefast/di";
602
+ import {
603
+ AmbiguousBindingError,
604
+ DiError,
605
+ ScopeViolationError,
606
+ TokenNotBoundError,
607
+ } from "@codefast/di";
550
608
 
551
609
  try {
552
610
  container.resolve(ServiceToken);
@@ -554,7 +612,11 @@ try {
554
612
  if (error instanceof TokenNotBoundError) {
555
613
  console.error(`Not registered: ${error.tokenName}`);
556
614
  } else if (error instanceof ScopeViolationError) {
557
- console.error(`Scope violation: ${error.message}`);
615
+ console.error(
616
+ `Scope violation: ${error.details.consumerToken} → ${error.details.dependencyToken}`,
617
+ );
618
+ } else if (error instanceof AmbiguousBindingError) {
619
+ console.error(`Ambiguous: ${error.tokenName}`, error.candidateIds);
558
620
  } else if (error instanceof DiError) {
559
621
  console.error(`DI error [${error.code}]: ${error.message}`);
560
622
  }
@@ -565,39 +627,41 @@ try {
565
627
 
566
628
  ## Package exports
567
629
 
568
- The root entry re-exports the full public API. Subpath exports are provided for fine-grained imports and bundler tree-shaking.
569
-
570
- | Subpath | Contents |
571
- | ---------------------------------------------- | ---------------------------------------------------------------- |
572
- | `@codefast/di` | Public façade — tokens, `Container`, modules, decorators, errors |
573
- | `@codefast/di/container` | `Container` interface and related types |
574
- | `@codefast/di/token` | `token()`, `Token<Value>`, `TokenValue` |
575
- | `@codefast/di/binding` | `BindingBuilder`, binding type definitions |
576
- | `@codefast/di/binding-select` | Binding selection internals (`filterMatchingBindings`, …) |
577
- | `@codefast/di/module` | `Module`, `AsyncModule`, module builders |
578
- | `@codefast/di/decorators/inject` | `inject`, `optional`, `injectAll`, `isInjectionDescriptor` |
579
- | `@codefast/di/decorators/injectable` | `@injectable`, `getAutoRegistered` |
580
- | `@codefast/di/decorators/lifecycle-decorators` | `@postConstruct`, `@preDestroy` |
581
- | `@codefast/di/constraints` | `whenParentIs`, `whenAnyAncestorIs`, `whenParentTagged` |
582
- | `@codefast/di/registry` | `BindingRegistry` |
583
- | `@codefast/di/resolver` | `DependencyResolver` internals |
584
- | `@codefast/di/scope` | `ScopeManager` |
585
- | `@codefast/di/lifecycle` | Activation/deactivation runners |
586
- | `@codefast/di/dependency-graph` | Dependency-edge collection helpers |
587
- | `@codefast/di/graph-adapters/cytoscape` | Cytoscape adapter for `ContainerGraphJson` |
588
- | `@codefast/di/graph-adapters/dot` | DOT adapter for `ContainerGraphJson` |
589
- | `@codefast/di/graph-adapters/reactflow` | React Flow adapter for `ContainerGraphJson` |
590
- | `@codefast/di/graph-adapters/types` | Shared graph adapter type definitions |
591
- | `@codefast/di/inspector` | `ContainerInspector`, snapshot + graph types |
592
- | `@codefast/di/errors` | Full `DiError` hierarchy |
593
- | `@codefast/di/environment` | `isDevelopmentOrTestEnvironment`, `isProductionEnvironment` |
594
- | `@codefast/di/metadata/metadata-keys` | Metadata symbol keys |
595
- | `@codefast/di/metadata/metadata-types` | Metadata type definitions |
596
- | `@codefast/di/metadata/param-registry` | Constructor parameter metadata registry |
597
- | `@codefast/di/metadata/symbol-metadata-reader` | Symbol metadata reader utilities |
598
- | `@codefast/di/package.json` | Package metadata |
599
-
600
- See `package.json → exports` for the authoritative list.
630
+ The root entry re-exports the full façade (`package.json` → `"."`). Subpaths mirror `package.json` → `exports` for tree-shaking.
631
+
632
+ | Subpath | Primary contents |
633
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
634
+ | `@codefast/di` | Tokens, `Container`, modules, decorators, errors, graph types, `MetadataReaderToken`, helpers from `binding-scope` / `resolve-options`, `createAutoRegisterRegistry` |
635
+ | `@codefast/di/binding` | `BindingBuilder` surface and binding model types |
636
+ | `@codefast/di/binding-scope` | `effectiveBindingScope` |
637
+ | `@codefast/di/binding-select` | `selectBinding`, `selectAllBindings` |
638
+ | `@codefast/di/constraints` | `whenParentIs`, `whenNoParentIs`, `whenAnyAncestorIs`, `whenNoAncestorIs`, `whenParentNamed`, `whenAnyAncestorNamed`, `whenParentTagged`, `whenAnyAncestorTagged` |
639
+ | `@codefast/di/constructor-type` | `Constructor`, `ConstructorInvocation` |
640
+ | `@codefast/di/container` | `Container`, `ContainerStatic` |
641
+ | `@codefast/di/decorators/inject` | `inject`, `optional`, `injectAll`, `isInjectionDescriptor`, descriptor types |
642
+ | `@codefast/di/decorators/injectable` | `injectable`, `createAutoRegisterRegistry`, `AutoRegisterRegistry` |
643
+ | `@codefast/di/decorators/lifecycle-decorators` | `postConstruct`, `preDestroy` |
644
+ | `@codefast/di/dependency-graph` | `buildDependencyGraph`, `ContainerGraphJson`, `GraphOptions`, … |
645
+ | `@codefast/di/environment` | `runWithContainer`, `getActiveContainer`, `DefaultResolutionContext`, `ResolverCallbacks`, `buildMaterializationFrame` |
646
+ | `@codefast/di/errors` | Full `DiError` hierarchy |
647
+ | `@codefast/di/graph-adapters/cytoscape` | `toCytoscapeGraph` |
648
+ | `@codefast/di/graph-adapters/dot` | `toDotGraph` |
649
+ | `@codefast/di/graph-adapters/reactflow` | `toReactFlowGraph` |
650
+ | `@codefast/di/graph-adapters/types` | Re-exports graph JSON types from `dependency-graph` |
651
+ | `@codefast/di/inspector` | `Inspector`, `BindingSnapshot`, `ContainerSnapshot` |
652
+ | `@codefast/di/lifecycle` | `LifecycleManager` |
653
+ | `@codefast/di/metadata/metadata-keys` | Metadata keys + `WeakMap` registries |
654
+ | `@codefast/di/metadata/metadata-reader-token` | `MetadataReaderToken` |
655
+ | `@codefast/di/metadata/metadata-types` | `MetadataReader`, lifecycle metadata types |
656
+ | `@codefast/di/metadata/symbol-metadata-reader` | `SymbolMetadataReader`, `defaultMetadataReader` |
657
+ | `@codefast/di/module` | `Module`, `AsyncModule`, `SyncModule`, `isSyncModule`, builders |
658
+ | `@codefast/di/registry` | `BindingRegistry` |
659
+ | `@codefast/di/resolve-options` | `injectableSlotToResolveOptions`, `slotKeyToResolveOptions` |
660
+ | `@codefast/di/resolver` | `DependencyResolver` |
661
+ | `@codefast/di/scope` | `ScopeManager` |
662
+ | `@codefast/di/token` | `token`, `Token`, `tokenName`, … |
663
+ | `@codefast/di/types` | Core DI types (`BindingScope`, `ResolutionContext`, `ResolveOptions`, …) |
664
+ | `@codefast/di/package.json` | Package manifest |
601
665
 
602
666
  ---
603
667
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Constructor } from "./constructor-type.mjs";
2
- import { Token, token } from "./token.mjs";
2
+ import { Token, isToken, token, tokenName } from "./token.mjs";
3
3
  import { ActivationHandler, BindingIdentifier, BindingKind, BindingScope, ConstraintContext, DeactivationHandler, DependencyKey, MaterializationFrame, ResolutionContext, ResolveOptions, TokenValue } from "./types.mjs";
4
4
  import { InjectOptions, InjectableDependency, InjectionDescriptor, inject, injectAll, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
5
5
  import { AliasBindingBuilder, BindToBuilder, BindingBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "./binding.mjs";
@@ -14,4 +14,4 @@ import { postConstruct, preDestroy } from "./decorators/lifecycle-decorators.mjs
14
14
  import { AmbiguousBindingError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, DisposedContainerError, InternalError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationDetails, ScopeViolationError, SyncDisposalNotSupportedError, TokenNotBoundError } from "./errors.mjs";
15
15
  import { injectableSlotToResolveOptions, slotKeyToResolveOptions } from "./resolve-options.mjs";
16
16
  import { MetadataReaderToken } from "./metadata/metadata-reader-token.mjs";
17
- export { type ActivationHandler, type AliasBindingBuilder, AmbiguousBindingError, AsyncDeactivationError, AsyncModule, type AsyncModuleBuilder, AsyncModuleLoadError, AsyncResolutionError, type AutoRegisterRegistry, type BindToBuilder, type BindingBuilder, type BindingIdentifier, type BindingKind, type BindingScope, type BindingSnapshot, CircularDependencyError, type ConstantBindingBuilder, type ConstraintContext, type Constructor, Container, type ContainerGraphJson, type Container as ContainerInterface, type ContainerSnapshot, type ContainerStatic, type DeactivationHandler, type DependencyKey, DiError, DisposedContainerError, type GraphEdge, type GraphNode, type GraphOptions, type InjectOptions, type InjectableDependency, type InjectableOptions, type InjectionDescriptor, InternalError, type MaterializationFrame, type MetadataReader, MetadataReaderToken, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, Module, type ModuleBuilder, type MutableLifecycleMetadata, NoMatchingBindingError, RebindUnboundTokenError, type ResolutionContext, type ResolveOptions, type ScopeViolationDetails, ScopeViolationError, type ScopedBindingBuilder, type SingletonBindingBuilder, type SingletonLifecycleBuilder, SyncDisposalNotSupportedError, SyncModule, type Token, TokenNotBoundError, type TokenValue, type TransientBindingBuilder, createAutoRegisterRegistry, effectiveBindingScope, inject, injectAll, injectable, injectableSlotToResolveOptions, isInjectionDescriptor, optional, postConstruct, preDestroy, slotKeyToResolveOptions, token };
17
+ export { type ActivationHandler, type AliasBindingBuilder, AmbiguousBindingError, AsyncDeactivationError, AsyncModule, type AsyncModuleBuilder, AsyncModuleLoadError, AsyncResolutionError, type AutoRegisterRegistry, type BindToBuilder, type BindingBuilder, type BindingIdentifier, type BindingKind, type BindingScope, type BindingSnapshot, CircularDependencyError, type ConstantBindingBuilder, type ConstraintContext, type Constructor, Container, type ContainerGraphJson, type Container as ContainerInterface, type ContainerSnapshot, type ContainerStatic, type DeactivationHandler, type DependencyKey, DiError, DisposedContainerError, type GraphEdge, type GraphNode, type GraphOptions, type InjectOptions, type InjectableDependency, type InjectableOptions, type InjectionDescriptor, InternalError, type MaterializationFrame, type MetadataReader, MetadataReaderToken, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, Module, type ModuleBuilder, type MutableLifecycleMetadata, NoMatchingBindingError, RebindUnboundTokenError, type ResolutionContext, type ResolveOptions, type ScopeViolationDetails, ScopeViolationError, type ScopedBindingBuilder, type SingletonBindingBuilder, type SingletonLifecycleBuilder, SyncDisposalNotSupportedError, SyncModule, type Token, TokenNotBoundError, type TokenValue, type TransientBindingBuilder, createAutoRegisterRegistry, effectiveBindingScope, inject, injectAll, injectable, injectableSlotToResolveOptions, isInjectionDescriptor, isToken, optional, postConstruct, preDestroy, slotKeyToResolveOptions, token, tokenName };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { effectiveBindingScope } from "./binding-scope.mjs";
2
2
  import { AmbiguousBindingError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, DisposedContainerError, InternalError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, TokenNotBoundError } from "./errors.mjs";
3
- import { token } from "./token.mjs";
3
+ import { isToken, token, tokenName } from "./token.mjs";
4
4
  import { injectableSlotToResolveOptions, slotKeyToResolveOptions } from "./resolve-options.mjs";
5
5
  import { MetadataReaderToken } from "./metadata/metadata-reader-token.mjs";
6
6
  import { inject, injectAll, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
@@ -8,4 +8,4 @@ import { AsyncModule, Module, SyncModule } from "./module.mjs";
8
8
  import { Container } from "./container.mjs";
9
9
  import { createAutoRegisterRegistry, injectable } from "./decorators/injectable.mjs";
10
10
  import { postConstruct, preDestroy } from "./decorators/lifecycle-decorators.mjs";
11
- export { AmbiguousBindingError, AsyncDeactivationError, AsyncModule, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, Container, DiError, DisposedContainerError, InternalError, MetadataReaderToken, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, Module, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, SyncModule, TokenNotBoundError, createAutoRegisterRegistry, effectiveBindingScope, inject, injectAll, injectable, injectableSlotToResolveOptions, isInjectionDescriptor, optional, postConstruct, preDestroy, slotKeyToResolveOptions, token };
11
+ export { AmbiguousBindingError, AsyncDeactivationError, AsyncModule, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, Container, DiError, DisposedContainerError, InternalError, MetadataReaderToken, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, Module, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, SyncModule, TokenNotBoundError, createAutoRegisterRegistry, effectiveBindingScope, inject, injectAll, injectable, injectableSlotToResolveOptions, isInjectionDescriptor, isToken, optional, postConstruct, preDestroy, slotKeyToResolveOptions, token, tokenName };
@@ -9,6 +9,7 @@ declare class BindingRegistry {
9
9
  private readonly _byId;
10
10
  private readonly _simpleNamed;
11
11
  private readonly _fastDefault;
12
+ private readonly _simpleTagged;
12
13
  /** Add or replace binding using slot-aware last-wins. */
13
14
  add(binding: Binding): void;
14
15
  /** Remove all bindings for a token. Returns removed bindings. */
@@ -26,9 +27,12 @@ declare class BindingRegistry {
26
27
  /** Remove all bindings. Returns all removed. */
27
28
  clear(): readonly Binding[];
28
29
  getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined;
30
+ getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined;
29
31
  getFastDefault(token: Token<unknown> | Constructor): Binding | undefined;
30
32
  /** Summarize available slot strings for a token (for error messages). */
31
33
  availableSlotStrings(t: Token<unknown> | Constructor): string[];
34
+ private _indexSimpleTaggedBinding;
35
+ private _deindexSimpleTaggedBinding;
32
36
  private _isPurePredicateBinding;
33
37
  private _indexSimpleNamedBinding;
34
38
  private _deindexSimpleNamedBinding;
package/dist/registry.mjs CHANGED
@@ -5,6 +5,7 @@ var BindingRegistry = class {
5
5
  _byId = /* @__PURE__ */ new Map();
6
6
  _simpleNamed = /* @__PURE__ */ new Map();
7
7
  _fastDefault = /* @__PURE__ */ new Map();
8
+ _simpleTagged = /* @__PURE__ */ new Map();
8
9
  /** Add or replace binding using slot-aware last-wins. */
9
10
  add(binding) {
10
11
  const key = binding.token;
@@ -24,6 +25,7 @@ var BindingRegistry = class {
24
25
  list.push(binding);
25
26
  this._byId.set(binding.id, binding);
26
27
  this._indexSimpleNamedBinding(key, binding);
28
+ this._indexSimpleTaggedBinding(key, binding);
27
29
  this._refreshFastDefaultForToken(key);
28
30
  }
29
31
  /** Remove all bindings for a token. Returns removed bindings. */
@@ -32,6 +34,7 @@ var BindingRegistry = class {
32
34
  const list = this._bindings.get(key) ?? [];
33
35
  this._bindings.delete(key);
34
36
  this._simpleNamed.delete(key);
37
+ this._simpleTagged.delete(key);
35
38
  this._fastDefault.delete(key);
36
39
  for (const b of list) this._byId.delete(b.id);
37
40
  return list;
@@ -47,9 +50,11 @@ var BindingRegistry = class {
47
50
  const idx = list.findIndex((b) => b.id === id);
48
51
  if (idx !== -1) list.splice(idx, 1);
49
52
  this._deindexSimpleNamedBinding(key, binding);
53
+ this._deindexSimpleTaggedBinding(key, binding);
50
54
  if (list.length === 0) {
51
55
  this._bindings.delete(key);
52
56
  this._simpleNamed.delete(key);
57
+ this._simpleTagged.delete(key);
53
58
  this._fastDefault.delete(key);
54
59
  } else this._refreshFastDefaultForToken(key);
55
60
  }
@@ -81,12 +86,16 @@ var BindingRegistry = class {
81
86
  this._bindings.clear();
82
87
  this._byId.clear();
83
88
  this._simpleNamed.clear();
89
+ this._simpleTagged.clear();
84
90
  this._fastDefault.clear();
85
91
  return all;
86
92
  }
87
93
  getSimpleNamed(token, name) {
88
94
  return this._simpleNamed.get(token)?.get(name);
89
95
  }
96
+ getSimpleTagged(token, tagKey, tagValue) {
97
+ return this._simpleTagged.get(token)?.get(tagKey)?.get(tagValue);
98
+ }
90
99
  getFastDefault(token) {
91
100
  return this._fastDefault.get(token);
92
101
  }
@@ -101,6 +110,38 @@ var BindingRegistry = class {
101
110
  return parts.join(",");
102
111
  });
103
112
  }
113
+ _indexSimpleTaggedBinding(tokenKeyValue, binding) {
114
+ const slot = binding.slot;
115
+ if (slot.name !== void 0 || slot.tags.length !== 1 || binding.predicate !== void 0) return;
116
+ const [tagKey, tagValue] = slot.tags[0];
117
+ let byTagKey = this._simpleTagged.get(tokenKeyValue);
118
+ if (byTagKey === void 0) {
119
+ byTagKey = /* @__PURE__ */ new Map();
120
+ this._simpleTagged.set(tokenKeyValue, byTagKey);
121
+ }
122
+ let byTagValue = byTagKey.get(tagKey);
123
+ if (byTagValue === void 0) {
124
+ byTagValue = /* @__PURE__ */ new Map();
125
+ byTagKey.set(tagKey, byTagValue);
126
+ }
127
+ byTagValue.set(tagValue, binding);
128
+ }
129
+ _deindexSimpleTaggedBinding(tokenKeyValue, binding) {
130
+ const slot = binding.slot;
131
+ if (slot.name !== void 0 || slot.tags.length !== 1 || binding.predicate !== void 0) return;
132
+ const [tagKey, tagValue] = slot.tags[0];
133
+ const byTagKey = this._simpleTagged.get(tokenKeyValue);
134
+ if (byTagKey === void 0) return;
135
+ const byTagValue = byTagKey.get(tagKey);
136
+ if (byTagValue === void 0) return;
137
+ if (byTagValue.get(tagValue)?.id === binding.id) {
138
+ byTagValue.delete(tagValue);
139
+ if (byTagValue.size === 0) {
140
+ byTagKey.delete(tagKey);
141
+ if (byTagKey.size === 0) this._simpleTagged.delete(tokenKeyValue);
142
+ }
143
+ }
144
+ }
104
145
  _isPurePredicateBinding(binding) {
105
146
  const slot = binding.slot;
106
147
  const hasPredicate = binding.predicate !== void 0;
package/dist/resolver.mjs CHANGED
@@ -54,6 +54,14 @@ var DependencyResolver = class {
54
54
  owner: this
55
55
  };
56
56
  }
57
+ if (hint !== void 0 && hint.name === void 0 && hint.tag === void 0 && (hint.tags?.length ?? 0) === 1) {
58
+ const [tagKey, tagValue] = hint.tags[0];
59
+ const tagged = this._registry.getSimpleTagged(token, tagKey, tagValue);
60
+ if (tagged !== void 0) return {
61
+ binding: tagged,
62
+ owner: this
63
+ };
64
+ }
57
65
  const bindings = this._registry.getAll(token);
58
66
  if (bindings.length > 0) {
59
67
  if (bindings.length === 1) {
@@ -233,12 +241,8 @@ var DependencyResolver = class {
233
241
  return resolved;
234
242
  }
235
243
  resolveOptional(token, hint, resolutionPath, materializationStack) {
236
- try {
237
- return this.resolve(token, hint, resolutionPath, materializationStack);
238
- } catch (e) {
239
- if (e instanceof TokenNotBoundError || e instanceof NoMatchingBindingError) return;
240
- throw e;
241
- }
244
+ if (this._findBinding(token, hint, resolutionPath, materializationStack) === void 0) return;
245
+ return this.resolve(token, hint, resolutionPath, materializationStack);
242
246
  }
243
247
  resolveAll(token, hint, resolutionPath, materializationStack) {
244
248
  if (hint?.name !== void 0 && hint.tag === void 0 && (hint.tags?.length ?? 0) === 0) {
@@ -424,12 +428,8 @@ var DependencyResolver = class {
424
428
  return Promise.all(pending);
425
429
  }
426
430
  async resolveOptionalAsync(token, hint, resolutionPath, materializationStack) {
427
- try {
428
- return await this.resolveAsync(token, hint, resolutionPath, materializationStack);
429
- } catch (e) {
430
- if (e instanceof TokenNotBoundError || e instanceof NoMatchingBindingError) return;
431
- throw e;
432
- }
431
+ if (this._findBinding(token, hint, resolutionPath, materializationStack) === void 0) return;
432
+ return this.resolveAsync(token, hint, resolutionPath, materializationStack);
433
433
  }
434
434
  async resolveAllAsync(token, hint, resolutionPath, materializationStack) {
435
435
  if (hint?.name !== void 0 && hint.tag === void 0 && (hint.tags?.length ?? 0) === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/di",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Lightweight dependency injection primitives for Codefast",
5
5
  "keywords": [
6
6
  "codefast",
@@ -171,13 +171,13 @@
171
171
  },
172
172
  "devDependencies": {
173
173
  "@types/node": "^25.6.0",
174
- "@typescript/native-preview": "7.0.0-dev.20260422.1",
174
+ "@typescript/native-preview": "7.0.0-dev.20260502.1",
175
175
  "@vitest/coverage-v8": "^4.1.5",
176
176
  "expect-type": "^1.3.0",
177
177
  "typescript": "^6.0.3",
178
178
  "unplugin-swc": "^1.5.9",
179
179
  "vitest": "^4.1.5",
180
- "@codefast/typescript-config": "0.3.14"
180
+ "@codefast/typescript-config": "0.3.15"
181
181
  },
182
182
  "engines": {
183
183
  "node": ">=22.0.0"
@@ -187,7 +187,6 @@
187
187
  "check-types": "tsgo --noEmit",
188
188
  "clean": "rm -rf dist",
189
189
  "examples": "for file in examples/*/*.ts; do echo \"=== Running $file ===\"; tsx \"$file\"; echo \"\n\" || exit 1; done",
190
- "bench": "vitest bench --run",
191
190
  "test": "vitest run",
192
191
  "test:coverage": "vitest run --coverage",
193
192
  "test:watch": "vitest"