@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 +8 -0
- package/README.md +173 -109
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/registry.d.mts +4 -0
- package/dist/registry.mjs +41 -0
- package/dist/resolver.mjs +12 -12
- package/package.json +3 -4
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
|
[](https://www.npmjs.com/package/@codefast/di)
|
|
8
8
|
[](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`
|
|
52
|
-
- **Fluent binding API.** Constants, classes, sync/async factories, aliases, named/tagged/predicate constraints, activation + deactivation hooks.
|
|
53
|
-
- **Module system.** `Module`
|
|
54
|
-
- **Scope
|
|
55
|
-
- **Async
|
|
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
|
-
|
|
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.
|
|
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([...])`
|
|
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
|
|
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
|
|
175
|
-
|
|
|
176
|
-
| `.toConstantValue(value)`
|
|
177
|
-
| `.toSelf()`
|
|
178
|
-
| `.to(Constructor)`
|
|
179
|
-
| `.toDynamic(factory)`
|
|
180
|
-
| `.toDynamicAsync(factory)`
|
|
181
|
-
| `.toResolved(factory, deps)`
|
|
182
|
-
| `.
|
|
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
|
|
256
|
-
|
|
|
257
|
-
| `whenParentIs(key)`
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
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)`
|
|
280
|
-
- `onDeactivation(instance)`
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
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.
|
|
338
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
container.loadAutoRegistered();
|
|
396
|
+
const container = Container.create();
|
|
397
|
+
container.loadAutoRegistered(autoRegister);
|
|
373
398
|
```
|
|
374
399
|
|
|
375
|
-
|
|
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
|
-
|
|
407
|
-
const cache = container.has(CacheToken) ? await container.resolveAsync(CacheToken) : undefined;
|
|
432
|
+
const cache = await container.resolveOptionalAsync(CacheToken);
|
|
408
433
|
|
|
409
|
-
// Eagerly construct
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
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
|
-
|
|
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({
|
|
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`
|
|
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
|
|
519
|
+
} // dispose() runs deactivation hooks for owned singletons
|
|
476
520
|
```
|
|
477
521
|
|
|
478
|
-
|
|
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*(…)
|
|
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),
|
|
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
|
-
|
|
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
|
|
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
|
|
538
|
-
|
|
|
539
|
-
| `
|
|
540
|
-
| `
|
|
541
|
-
| `
|
|
542
|
-
| `
|
|
543
|
-
| `
|
|
544
|
-
| `
|
|
545
|
-
| `
|
|
546
|
-
| `
|
|
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 {
|
|
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(
|
|
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
|
|
569
|
-
|
|
570
|
-
| Subpath |
|
|
571
|
-
| ---------------------------------------------- |
|
|
572
|
-
| `@codefast/di` |
|
|
573
|
-
| `@codefast/di/
|
|
574
|
-
| `@codefast/di/
|
|
575
|
-
| `@codefast/di/binding`
|
|
576
|
-
| `@codefast/di/
|
|
577
|
-
| `@codefast/di/
|
|
578
|
-
| `@codefast/di/
|
|
579
|
-
| `@codefast/di/decorators/
|
|
580
|
-
| `@codefast/di/decorators/
|
|
581
|
-
| `@codefast/di/
|
|
582
|
-
| `@codefast/di/
|
|
583
|
-
| `@codefast/di/
|
|
584
|
-
| `@codefast/di/
|
|
585
|
-
| `@codefast/di/
|
|
586
|
-
| `@codefast/di/
|
|
587
|
-
| `@codefast/di/graph-adapters/
|
|
588
|
-
| `@codefast/di/graph-adapters/
|
|
589
|
-
| `@codefast/di/
|
|
590
|
-
| `@codefast/di/
|
|
591
|
-
| `@codefast/di/
|
|
592
|
-
| `@codefast/di/
|
|
593
|
-
| `@codefast/di/
|
|
594
|
-
| `@codefast/di/metadata/metadata-
|
|
595
|
-
| `@codefast/di/
|
|
596
|
-
| `@codefast/di/
|
|
597
|
-
| `@codefast/di/
|
|
598
|
-
| `@codefast/di/
|
|
599
|
-
|
|
600
|
-
|
|
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 };
|
package/dist/registry.d.mts
CHANGED
|
@@ -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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|