@decohere/crucible 0.0.1-dev.20 → 0.0.1-dev.20261105.103
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/LICENSE +15 -0
- package/README.md +354 -0
- package/es6/src/const.js +3 -5
- package/es6/src/decorators/index.js +0 -2
- package/es6/src/decorators/inject.decorator.js +27 -14
- package/es6/src/decorators/injectable.dectorator.js +0 -2
- package/es6/src/index.js +0 -2
- package/es6/src/models/service-key.js +10 -0
- package/es6/src/models/service-registration.model.js +1 -6
- package/es6/src/services/container.js +197 -0
- package/es6/src/services/index.js +2 -4
- package/es6/src/services/service-container-builder.js +57 -0
- package/es6/src/services/service-registry.js +34 -23
- package/es6/src/types/errors.js +0 -2
- package/es6/src/types/index.js +1 -2
- package/es6/src/types/types.js +0 -2
- package/package.json +13 -7
- package/types/src/const.d.ts +1 -1
- package/types/src/decorators/inject.decorator.d.ts +17 -2
- package/types/src/models/service-key.d.ts +9 -0
- package/types/src/models/service-registration.model.d.ts +5 -4
- package/types/src/services/{crucible.d.ts → container.d.ts} +16 -5
- package/types/src/services/index.d.ts +2 -2
- package/types/src/services/{crucible-builder.d.ts → service-container-builder.d.ts} +9 -5
- package/types/src/services/service-registry.d.ts +12 -6
- package/types/src/types/index.d.ts +1 -0
- package/types/src/types/types.d.ts +1 -1
- package/commonjs/src/const.js +0 -33
- package/commonjs/src/decorators/index.js +0 -21
- package/commonjs/src/decorators/inject.decorator.js +0 -27
- package/commonjs/src/decorators/injectable.dectorator.js +0 -16
- package/commonjs/src/index.js +0 -22
- package/commonjs/src/models/registration-key.model.js +0 -31
- package/commonjs/src/models/service-registration.model.js +0 -30
- package/commonjs/src/services/crucible-builder.js +0 -68
- package/commonjs/src/services/crucible.js +0 -165
- package/commonjs/src/services/index.js +0 -21
- package/commonjs/src/services/service-registry.js +0 -50
- package/commonjs/src/types/errors.js +0 -29
- package/commonjs/src/types/index.js +0 -20
- package/commonjs/src/types/types.js +0 -6
- package/es6/src/models/registration-key.model.js +0 -21
- package/es6/src/services/crucible-builder.js +0 -58
- package/es6/src/services/crucible.js +0 -155
- package/types/src/models/registration-key.model.d.ts +0 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Typesmiths
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# @decohere/crucible
|
|
2
|
+
|
|
3
|
+
Lightweight dependency injection for Decohere. Register classes (or tokens) on a `ServiceContainerBuilder`, `build()` a container, then `resolve()` graphs of constructor-injected services.
|
|
4
|
+
|
|
5
|
+
Higher layers (for example `@decohere/http`'s `Builder.withServices`) register on a `ServiceContainerBuilder`. You can also build and resolve from a container yourself.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
| Use `@decohere/crucible` | Do not use it |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Constructor-injected services and controllers | Replacing the HTTP, config or data packages — those compose *on top of* the container |
|
|
12
|
+
| Tokens for interfaces, config objects, and “many of this kind” | Auto-scanning folders or Nest-style modules (not in this package) |
|
|
13
|
+
| Request / singleton / transient lifetimes and `using` disposal | Compile-time graph checking (the graph is checked when a service is first resolved) |
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i @decohere/crucible
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { ServiceContainerBuilder, LifetimeScopeToken, Scopes } from "@decohere/crucible";
|
|
23
|
+
import { inject, injectable } from "@decohere/crucible/decorators";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Decorator metadata is required. Enable it in `tsconfig.json` (the Decohere scaffold and this repo already do):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"compilerOptions": {
|
|
31
|
+
"experimentalDecorators": true,
|
|
32
|
+
"emitDecoratorMetadata": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@decohere/core/reflection` loads `reflect-metadata`. Import `@decohere/crucible` (or `@decohere/core`) before constructing decorated classes.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Constructor injection — `@inject` is usually unnecessary
|
|
42
|
+
|
|
43
|
+
If the constructor parameter is a **concrete class**, the container reads `design:paramtypes` and resolves that class. Mark the *consumer* `@injectable()` so TypeScript/SWC emit that metadata. Do **not** add `@inject` for that parameter.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { injectable } from "@decohere/crucible/decorators";
|
|
47
|
+
import { SampleService } from "./sample.service.js";
|
|
48
|
+
|
|
49
|
+
@injectable()
|
|
50
|
+
export class HelloController {
|
|
51
|
+
public constructor(private readonly sample: SampleService) {}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { ServiceContainerBuilder } from "@decohere/crucible";
|
|
57
|
+
|
|
58
|
+
const builder = new ServiceContainerBuilder();
|
|
59
|
+
builder.registerSingleton(SampleService);
|
|
60
|
+
builder.registerRequestScoped(HelloController);
|
|
61
|
+
const container = builder.build();
|
|
62
|
+
|
|
63
|
+
using scope = container.beginScope("req");
|
|
64
|
+
const hello = scope.resolve(HelloController);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`@injectable()` is an empty class decorator. Its job is to force `design:paramtypes` onto the constructor. Any other class decorator (`@Controller()`, `@Trace`, …) also does that; `@injectable()` is the one to use when there is nothing else.
|
|
68
|
+
|
|
69
|
+
### Do not `import type` injected classes
|
|
70
|
+
|
|
71
|
+
`import type` is erased at compile time. `emitDecoratorMetadata` then records `Object` (or nothing) instead of the constructor. The container cannot tell what to inject and **throws `ResolveError`** when the consumer is resolved, naming the parameter and telling you to add `@inject`.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// Wrong — SampleService does not exist at runtime, paramtypes collapse to Object
|
|
75
|
+
import type { SampleService } from "./sample.service.js";
|
|
76
|
+
|
|
77
|
+
// Right — value import keeps the constructor for reflect-metadata
|
|
78
|
+
import { SampleService } from "./sample.service.js";
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`import type` is correct for **interfaces** and type-only helpers. Those have no runtime constructor; pair them with `@inject(token)` (see below).
|
|
82
|
+
|
|
83
|
+
Linters that prefer type-only imports (`import type` / `import { type X }`) will flag a class that appears unused except as a constructor type. Keep the value import and suppress that rule locally — for example with oxlint (or Biome, or ESLint):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// oxlint-disable-next-line typescript/consistent-type-imports -- constructor injection needs the class at runtime
|
|
87
|
+
import { Workspace } from "@decohere/workspace";
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
That pattern is used throughout Forge (`Build`, `Clean`, …). Same idea applies to ESLint `consistent-type-imports` or equivalent.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## When you *do* need `@inject`
|
|
95
|
+
|
|
96
|
+
`@inject(key)` overrides the paramtypes lookup with an explicit key: a class constructor, string, or symbol. Use it when there is no useful constructor in `design:paramtypes`.
|
|
97
|
+
|
|
98
|
+
| Situation | Why `@inject` |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Interface or type alias (`ISampleStore`) | No runtime class; paramtypes is `Object` |
|
|
101
|
+
| String / symbol token | The service is registered under the token, not the class |
|
|
102
|
+
| Array of implementations (`Transformer[]`) | Paramtypes is `Array`, which says nothing about the element type |
|
|
103
|
+
| Paramtypes collapsed to `Object` (the class's import was dropped) | `@inject(TheClass)` uses the class as a value, so its import is kept |
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { inject, injectable } from "@decohere/crucible/decorators";
|
|
107
|
+
|
|
108
|
+
export const SampleStoreToken = Symbol("SampleStore");
|
|
109
|
+
|
|
110
|
+
export interface ISampleStore {
|
|
111
|
+
get(id: string): Promise<unknown>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@injectable()
|
|
115
|
+
export class SampleService {
|
|
116
|
+
public constructor(
|
|
117
|
+
@inject(SampleStoreToken) private readonly store: ISampleStore,
|
|
118
|
+
) {}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Keep tokens in a `const.ts` (or next to the interface). Register against the same token:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
builder.registerSingleton(SampleStoreToken, PostgresSampleStore);
|
|
126
|
+
// or an already-built instance:
|
|
127
|
+
builder.registerSingleton(SampleStoreToken, store);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`@inject` takes `string | symbol | Constructable`. To inject a class, pass the class: `@inject("SampleService")` does **not** find `SampleService` (see [Keys](#registration-and-lifetimes)). Arrays use `resolveMany` automatically when the reflected type is `Array`. For a dependency that may be absent, add `@optional()` (see [Optional / missing](#optional--missing-dependencies)).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Registration and lifetimes
|
|
135
|
+
|
|
136
|
+
Keys match by **identity** ([ADR 0013](../../docs/adr/0013-container-keys-and-lifetimes.md)): a class matches only itself, a symbol only itself, a string only an equal string. So `resolve("SampleService")` does not find `registerSingleton(SampleService)`, `Symbol("TokenService")` does not collide with a class `TokenService`, two classes with the same name in different modules stay apart, and minified class names still work.
|
|
137
|
+
|
|
138
|
+
Registering the same class (or the same type on a token) twice **throws** `RegistrationError` — silent last-wins hides accidental double-registers. Replace it explicitly:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
builder.override.registerSingleton(SampleService);
|
|
142
|
+
builder.override.registerTransient(SampleService);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`builder.override` is a registrar with replace already bound — the builder itself is not mutated. Two different types on one token is still multi-bind: `resolve` throws `ResolveError`, `resolveMany` returns both.
|
|
146
|
+
|
|
147
|
+
| Lifetime | Cached |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Singleton | On the root container — shared with every nested scope |
|
|
150
|
+
| Request | On the current scope — new instance per `beginScope` |
|
|
151
|
+
| Transient | Never — new instance every `resolve` |
|
|
152
|
+
|
|
153
|
+
A singleton may not depend on a request-scoped service, directly or through transients: it would keep the first request's instance forever (a "captive dependency", e.g. one database connection for the life of the process). Resolving one throws `ResolveError` naming the chain. Make the consumer request-scoped, or inject `LifetimeScopeToken` and resolve the request-scoped service per request. `LifetimeScopeToken` itself is allowed: a singleton gets the root container.
|
|
154
|
+
|
|
155
|
+
A dependency cycle (`A → B → A`) throws `ResolveError` listing the cycle.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
builder.registerSingleton(SampleService);
|
|
159
|
+
builder.registerSingleton(Token, SampleService);
|
|
160
|
+
builder.registerSingleton(Token, existingInstance);
|
|
161
|
+
|
|
162
|
+
builder.registerRequestScoped(HelloController);
|
|
163
|
+
builder.registerRequestScoped(Token, HelloController);
|
|
164
|
+
builder.registerRequestScoped(Token, () => createConnection());
|
|
165
|
+
|
|
166
|
+
builder.registerTransient(Helper);
|
|
167
|
+
builder.registerTransient(Token, Helper);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`registerType(type, scope?)` and `registerTypes(...)` are the same registrations without the lifetime in the method name (default **transient**). `Scopes.singleton` is the string `"single-instance"`.
|
|
171
|
+
|
|
172
|
+
HTTP (`@decohere/http`): controllers are **request-scoped** (one per request). Services are usually **singletons**.
|
|
173
|
+
|
|
174
|
+
`registerSingleton(Class, instance)` binds an existing object under the class. The CLI does this for the workspace:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
builder.registerSingleton(workspace.constructor, workspace);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Recipes
|
|
183
|
+
|
|
184
|
+
### Many implementations of one token
|
|
185
|
+
|
|
186
|
+
Register several types under the same token. Inject them as an array with `@inject(Token)`.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
export const TransformerKey = Symbol("TransformerKey");
|
|
190
|
+
|
|
191
|
+
builder.registerSingleton(TransformerKey, SwcTranspiler);
|
|
192
|
+
builder.registerSingleton(TransformerKey, TypeEmitter);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
@injectable()
|
|
197
|
+
export class Build {
|
|
198
|
+
public constructor(
|
|
199
|
+
@inject(TransformerKey) private readonly transformers: Transformer[],
|
|
200
|
+
) {}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`resolve(TransformerKey)` **throws** `ResolveError` when more than one registration exists. Use `resolveMany(TransformerKey)` or constructor injection of an array.
|
|
205
|
+
|
|
206
|
+
### Request scopes and disposal
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
builder.registerRequestScoped(UnitOfWork);
|
|
210
|
+
builder.registerSingleton(Clock);
|
|
211
|
+
const container = builder.build();
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
using scope = container.beginScope("req-1");
|
|
215
|
+
const uow = scope.resolve(UnitOfWork); // per-scope
|
|
216
|
+
const clock = scope.resolve(Clock); // same singleton as the root
|
|
217
|
+
} // scope[Symbol.dispose]() — request-scoped instances are disposed
|
|
218
|
+
|
|
219
|
+
await using scope = container.beginScope("req-2");
|
|
220
|
+
// async teardown via Symbol.asyncDispose if the instance implements it
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- Child scopes look registrations up in their parent (nothing is copied, so `beginScope` costs the same however many services are registered); they do not share the request-instance cache.
|
|
224
|
+
- Nested `beginScope` is allowed; each level has its own request instances.
|
|
225
|
+
- Named scopes: a live duplicate name throws `DuplicateScopeError`. After dispose, the name can be reused. Unnamed scopes do not collide.
|
|
226
|
+
- Disposing a parent does **not** dispose child scopes. Dispose the scope you created.
|
|
227
|
+
- Singletons are cached on the root container and shared with nested scopes. Resolving a singleton after the root is disposed throws.
|
|
228
|
+
|
|
229
|
+
`App.dispose` (`@decohere/http`) (Flux HMR / SIGINT) disposes the **root** container, then Scribe. Implement `Symbol.asyncDispose` (or `Symbol.dispose`) on long-lived services — sockets, pools, timers. Only **constructed** instances are disposed (resolve them at boot; transients are not cached). `export default app` is the composition-root contract; do not wrap it for extra teardown.
|
|
230
|
+
|
|
231
|
+
The current scope is always registered as `LifetimeScopeToken`:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { type LifetimeScope, LifetimeScopeToken } from "@decohere/crucible";
|
|
235
|
+
import { inject, injectable } from "@decohere/crucible/decorators";
|
|
236
|
+
|
|
237
|
+
@injectable()
|
|
238
|
+
export class ExceptionFilterHost {
|
|
239
|
+
public constructor(
|
|
240
|
+
@inject(LifetimeScopeToken) private readonly scope: LifetimeScope,
|
|
241
|
+
) {}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Factories
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
builder.registerRequestScoped("connection", () => createConnection());
|
|
249
|
+
const connection = container.resolve("connection");
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Arrow factories are called as functions (not `new`). Use them when construction needs arguments the container should not know about.
|
|
253
|
+
|
|
254
|
+
### Optional / missing dependencies
|
|
255
|
+
|
|
256
|
+
A missing registration **throws** `ResolveError`, naming the chain that needed it (`App → Router → Symbol(AuthorizationService)`). `resolveMany` returns `[]` instead. A `?` on the parameter type does not make it optional to the container; add `@optional()`, and the container passes `undefined` (so a default value applies):
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { inject, injectable, optional } from "@decohere/crucible/decorators";
|
|
260
|
+
|
|
261
|
+
@injectable()
|
|
262
|
+
export class App {
|
|
263
|
+
public constructor(
|
|
264
|
+
@inject(HttpProviderToken) @optional() private readonly http?: HttpProvider,
|
|
265
|
+
) {}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### HTTP apps
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { Builder } from "@decohere/http";
|
|
273
|
+
import { FastifyProvider } from "@decohere/http/providers";
|
|
274
|
+
import { SampleService } from "./services/sample.service.js";
|
|
275
|
+
import { HelloController } from "./controllers/hello.controller.js";
|
|
276
|
+
|
|
277
|
+
await Builder.create()
|
|
278
|
+
.withHttp(FastifyProvider)
|
|
279
|
+
.withControllers(HelloController)
|
|
280
|
+
.withServices((cb) => {
|
|
281
|
+
cb.registerSingleton(SampleService);
|
|
282
|
+
})
|
|
283
|
+
.buildAndStart();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`withHttp` / `withControllers` already register the HTTP stack (router, controllers as request-scoped, …). Application services go in `withServices`.
|
|
287
|
+
|
|
288
|
+
### Tests
|
|
289
|
+
|
|
290
|
+
Prefer constructing the subject with explicit fakes when you do not need the container. Use `ServiceContainerBuilder` when the test *is* the wiring (scopes, tokens, many-implementations).
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const builder = new ServiceContainerBuilder();
|
|
294
|
+
builder.registerSingleton(SampleService, fakeSample);
|
|
295
|
+
builder.registerRequestScoped(HelloController);
|
|
296
|
+
const container = builder.build();
|
|
297
|
+
using scope = container.beginScope("test");
|
|
298
|
+
const hello = scope.resolve(HelloController);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
To assert constructor arguments under DI, use `spyOnConstructor` from `@decohere/testing` — a bare `vi.spyOn` / `vi.fn` constructor drops `design:paramtypes`. See [Asserting DI constructor arguments](../../docs/testing/how-tos/vitest-constructor-spy-with-di.md).
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Public API
|
|
306
|
+
|
|
307
|
+
### `@decohere/crucible`
|
|
308
|
+
|
|
309
|
+
| Export | Role |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `ServiceContainerBuilder` | Fluent registration; `build()` → `Container` |
|
|
312
|
+
| `Container` | The container: `resolve`, `resolveMany`, `beginScope`, `Symbol.dispose` / `Symbol.asyncDispose` |
|
|
313
|
+
| `IContainer` / `LifetimeScope` / `ContainerBuilder` / `ServiceProvider` | Composition interfaces |
|
|
314
|
+
| `Scopes` | `{ request, singleton: "single-instance", transient }` |
|
|
315
|
+
| `LifetimeScopeToken` | Symbol for the current scope |
|
|
316
|
+
| `ResolveError` | Nothing registered, several registrations for `resolve`, a cycle, a captive dependency, or a parameter with no usable type |
|
|
317
|
+
| `DuplicateScopeError` | `beginScope` with a live duplicate name |
|
|
318
|
+
| `RegistrationError` | Same type registered twice without `builder.override` |
|
|
319
|
+
|
|
320
|
+
### `@decohere/crucible/decorators`
|
|
321
|
+
|
|
322
|
+
| Export | Role |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `injectable()` | Empty class decorator; emit `design:paramtypes` |
|
|
325
|
+
| `inject(key)` | Parameter decorator; resolve this key instead of the reflected type |
|
|
326
|
+
| `optional()` | Parameter decorator; pass `undefined` when nothing is registered instead of throwing |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Pitfalls
|
|
331
|
+
|
|
332
|
+
1. **`import type` on an injected class** — paramtypes become `Object` → `ResolveError` asking for `@inject`. Use a value import, or `@inject(TheClass)`.
|
|
333
|
+
2. **Missing `@injectable()` (or any decorator) on the consumer** — no paramtypes, constructor is called with zero arguments.
|
|
334
|
+
3. **`resolve` of a multi-bound token** — throws; use `resolveMany` or inject `T[]` with `@inject(token)`.
|
|
335
|
+
4. **Same type registered twice** — throws `RegistrationError`. Use `builder.override.registerSingleton(...)` (tests, plugin replacements).
|
|
336
|
+
5. **A string key for a class** — `@inject("SampleService")` does not find `SampleService`. Pass the class.
|
|
337
|
+
6. **Two copies of a package** — the class from one copy is not the class from the other, so a service registered with one is not found with the other. Install each package once.
|
|
338
|
+
7. **A singleton depending on a request-scoped service** — throws; see [lifetimes](#registration-and-lifetimes).
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Non-goals
|
|
343
|
+
|
|
344
|
+
- Folder scanning / “declare a module and everything is registered”
|
|
345
|
+
- Property or setter injection (constructors only)
|
|
346
|
+
- Compile-time graph checking
|
|
347
|
+
|
|
348
|
+
## Dependencies
|
|
349
|
+
|
|
350
|
+
| Package | Role |
|
|
351
|
+
|---|---|
|
|
352
|
+
| [`@decohere/core`](../core) | Reflection helpers for `design:paramtypes` (`getMeta`, `isConstructable`, …) |
|
|
353
|
+
|
|
354
|
+
Layering: [architecture](../../docs/architecture/).
|
package/es6/src/const.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const Container = "Container";
|
|
2
2
|
export const LifetimeScopeToken = Symbol("LifetimeScope");
|
|
3
|
-
export const
|
|
4
|
-
injecttoken: `${
|
|
3
|
+
export const ContainerMeta = {
|
|
4
|
+
injecttoken: `${Container}:injecttoken`
|
|
5
5
|
};
|
|
6
6
|
export const Scopes = {
|
|
7
7
|
request: "request",
|
|
8
8
|
singleton: "single-instance",
|
|
9
9
|
transient: "transient"
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9jb25zdC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFNjb3BlIH0gZnJvbSBcIi4vdHlwZXMvdHlwZXMuanNcIjtcblxuY29uc3QgQ3J1Y2libGUgPSBcIkNydWNpYmxlXCI7XG50eXBlIENydWNpYmxlTWV0YSA9IFwiaW5qZWN0dG9rZW5cIjtcblxuZXhwb3J0IGNvbnN0IExpZmV0aW1lU2NvcGVUb2tlbiA9IFN5bWJvbChcIkxpZmV0aW1lU2NvcGVcIik7XG5cbmV4cG9ydCBjb25zdCBDcnVjaWJsZU1ldGEgPSB7XG5cdGluamVjdHRva2VuOiBgJHtDcnVjaWJsZX06aW5qZWN0dG9rZW5gLFxufSBzYXRpc2ZpZXMgUmVjb3JkPENydWNpYmxlTWV0YSwgc3RyaW5nPjtcblxuZXhwb3J0IGNvbnN0IFNjb3BlcyA9IHtcblx0cmVxdWVzdDogXCJyZXF1ZXN0XCIsXG5cdHNpbmdsZXRvbjogXCJzaW5nbGUtaW5zdGFuY2VcIixcblx0dHJhbnNpZW50OiBcInRyYW5zaWVudFwiLFxufSBzYXRpc2ZpZXMgUmVjb3JkPHN0cmluZywgU2NvcGU+O1xuIl0sIm5hbWVzIjpbIkNydWNpYmxlIiwiTGlmZXRpbWVTY29wZVRva2VuIiwiU3ltYm9sIiwiQ3J1Y2libGVNZXRhIiwiaW5qZWN0dG9rZW4iLCJTY29wZXMiLCJyZXF1ZXN0Iiwic2luZ2xldG9uIiwidHJhbnNpZW50Il0sIm1hcHBpbmdzIjoiQUFFQSxNQUFNQSxXQUFXO0FBR2pCLE9BQU8sTUFBTUMscUJBQXFCQyxPQUFPLGlCQUFpQjtBQUUxRCxPQUFPLE1BQU1DLGVBQWU7SUFDM0JDLGFBQWEsR0FBR0osU0FBUyxZQUFZLENBQUM7QUFDdkMsRUFBeUM7QUFFekMsT0FBTyxNQUFNSyxTQUFTO0lBQ3JCQyxTQUFTO0lBQ1RDLFdBQVc7SUFDWEMsV0FBVztBQUNaLEVBQWtDIn0=
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
export * from "./inject.decorator.js";
|
|
2
2
|
export * from "./injectable.dectorator.js";
|
|
3
|
-
|
|
4
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9kZWNvcmF0b3JzL2luZGV4LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCAqIGZyb20gXCIuL2luamVjdC5kZWNvcmF0b3IuanNcIjtcbmV4cG9ydCAqIGZyb20gXCIuL2luamVjdGFibGUuZGVjdG9yYXRvci5qc1wiO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsd0JBQXdCO0FBQ3RDLGNBQWMsNkJBQTZCIn0=
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { defineMeta, getMeta } from "@decohere/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { defineMeta, getMeta } from "@decohere/core/reflection";
|
|
2
|
+
import { ContainerMeta } from "../const.js";
|
|
3
|
+
/**
|
|
4
|
+
* Bind a constructor parameter to a registration key. Keys match by identity: a class matches only
|
|
5
|
+
* itself (not a string of its name), and a symbol only itself. Prefer the class; use a symbol for an
|
|
6
|
+
* interface or several implementations. `@inject(SomeClass)` also keeps SomeClass's import, which
|
|
7
|
+
* a parameter used only as a type can lose (its run-time type then collapses to `Object`).
|
|
8
|
+
*/ export function inject(key) {
|
|
9
|
+
return updateToken((token)=>({
|
|
10
|
+
...token,
|
|
7
11
|
key
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mark a constructor parameter as optional: when nothing is registered for it, the container
|
|
16
|
+
* passes `undefined` (so a default value applies) instead of throwing.
|
|
17
|
+
*/ export function optional() {
|
|
18
|
+
return updateToken((token)=>({
|
|
19
|
+
...token,
|
|
20
|
+
optional: true
|
|
21
|
+
}));
|
|
11
22
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
function updateToken(update) {
|
|
24
|
+
return (target, _propertyKey, idx)=>{
|
|
25
|
+
// Copy: the metadata can be inherited from a base class, which must not change.
|
|
26
|
+
const metadata = new Map(getMeta(ContainerMeta.injecttoken, target));
|
|
27
|
+
metadata.set(idx, update(metadata.get(idx)));
|
|
28
|
+
defineMeta(ContainerMeta.injecttoken, metadata, target);
|
|
29
|
+
};
|
|
15
30
|
}
|
|
16
|
-
|
|
17
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9kZWNvcmF0b3JzL2luamVjdC5kZWNvcmF0b3IudHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZGVmaW5lTWV0YSwgZ2V0TWV0YSB9IGZyb20gXCJAZGVjb2hlcmUvbmV4dXMvcmVmbGVjdGlvblwiO1xuaW1wb3J0IHsgQ3J1Y2libGVNZXRhIH0gZnJvbSBcIi4uL2NvbnN0LmpzXCI7XG5cbmV4cG9ydCB0eXBlIEluamVjdGlvbk1ldGFkYXRhID0gTWFwPG51bWJlciwgSW5qZWN0aW9uVG9rZW4+O1xuXG5leHBvcnQgdHlwZSBJbmplY3Rpb25Ub2tlbiA9IHtcblx0a2V5OiBzdHJpbmcgfCBzeW1ib2w7XG59O1xuXG5leHBvcnQgZnVuY3Rpb24gaW5qZWN0KGtleTogc3RyaW5nIHwgc3ltYm9sKSB7XG5cdHJldHVybiAodGFyZ2V0OiBvYmplY3QsIF9wcm9wZXJ0eUtleTogc3RyaW5nIHwgc3ltYm9sIHwgdW5kZWZpbmVkLCBpZHg6IG51bWJlcikgPT4ge1xuXHRcdGNvbnN0IG1ldGFkYXRhID0gZ2V0RXhpc3RpbmdNZXRhKHRhcmdldCk7XG5cdFx0bWV0YWRhdGEuc2V0KGlkeCwgeyBrZXkgfSk7XG5cdFx0ZGVmaW5lTWV0YShDcnVjaWJsZU1ldGEuaW5qZWN0dG9rZW4sIG1ldGFkYXRhLCB0YXJnZXQpO1xuXHR9O1xufVxuXG5mdW5jdGlvbiBnZXRFeGlzdGluZ01ldGEodGFyZ2V0OiBvYmplY3QpIHtcblx0Y29uc3QgbWV0YSA9XG5cdFx0Z2V0TWV0YTxNYXA8bnVtYmVyLCBJbmplY3Rpb25Ub2tlbj4+KENydWNpYmxlTWV0YS5pbmplY3R0b2tlbiwgdGFyZ2V0KSA/P1xuXHRcdG5ldyBNYXA8bnVtYmVyLCBJbmplY3Rpb25Ub2tlbj4oKTtcblx0cmV0dXJuIG1ldGE7XG59XG4iXSwibmFtZXMiOlsiZGVmaW5lTWV0YSIsImdldE1ldGEiLCJDcnVjaWJsZU1ldGEiLCJpbmplY3QiLCJrZXkiLCJ0YXJnZXQiLCJfcHJvcGVydHlLZXkiLCJpZHgiLCJtZXRhZGF0YSIsImdldEV4aXN0aW5nTWV0YSIsInNldCIsImluamVjdHRva2VuIiwibWV0YSIsIk1hcCJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsVUFBVSxFQUFFQyxPQUFPLFFBQVEsNkJBQTZCO0FBQ2pFLFNBQVNDLFlBQVksUUFBUSxjQUFjO0FBUTNDLE9BQU8sU0FBU0MsT0FBT0MsR0FBb0I7SUFDMUMsT0FBTyxDQUFDQyxRQUFnQkMsY0FBMkNDO1FBQ2xFLE1BQU1DLFdBQVdDLGdCQUFnQko7UUFDakNHLFNBQVNFLEdBQUcsQ0FBQ0gsS0FBSztZQUFFSDtRQUFJO1FBQ3hCSixXQUFXRSxhQUFhUyxXQUFXLEVBQUVILFVBQVVIO0lBQ2hEO0FBQ0Q7QUFFQSxTQUFTSSxnQkFBZ0JKLE1BQWM7SUFDdEMsTUFBTU8sT0FDTFgsUUFBcUNDLGFBQWFTLFdBQVcsRUFBRU4sV0FDL0QsSUFBSVE7SUFDTCxPQUFPRDtBQUNSIn0=
|
|
@@ -2,5 +2,3 @@ export function injectable() {
|
|
|
2
2
|
// no implementation for now. Just used to ensure reflect metadata is generated
|
|
3
3
|
return ()=>{};
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9kZWNvcmF0b3JzL2luamVjdGFibGUuZGVjdG9yYXRvci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gaW5qZWN0YWJsZSgpOiBDbGFzc0RlY29yYXRvciB7XG5cdC8vIG5vIGltcGxlbWVudGF0aW9uIGZvciBub3cuIEp1c3QgdXNlZCB0byBlbnN1cmUgcmVmbGVjdCBtZXRhZGF0YSBpcyBnZW5lcmF0ZWRcblx0cmV0dXJuICgpID0+IHt9O1xufVxuIl0sIm5hbWVzIjpbImluamVjdGFibGUiXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sU0FBU0E7SUFDZiwrRUFBK0U7SUFDL0UsT0FBTyxLQUFPO0FBQ2YifQ==
|
package/es6/src/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export * from "./const.js";
|
|
2
2
|
export * from "./services/index.js";
|
|
3
3
|
export * from "./types/index.js";
|
|
4
|
-
|
|
5
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9pbmRleC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgKiBmcm9tIFwiLi9jb25zdC5qc1wiO1xuZXhwb3J0ICogZnJvbSBcIi4vc2VydmljZXMvaW5kZXguanNcIjtcbmV4cG9ydCAqIGZyb20gXCIuL3R5cGVzL2luZGV4LmpzXCI7XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxhQUFhO0FBQzNCLGNBQWMsc0JBQXNCO0FBQ3BDLGNBQWMsbUJBQW1CIn0=
|
|
@@ -9,12 +9,7 @@ export class ServiceRegistration {
|
|
|
9
9
|
this.scope = scope;
|
|
10
10
|
this.isInstance = isInstance;
|
|
11
11
|
}
|
|
12
|
-
isEqual(other) {
|
|
13
|
-
if (other.isInstance) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
12
|
+
/** Same class, or the same instance object. */ isEqual(other) {
|
|
16
13
|
return this.type === other.type;
|
|
17
14
|
}
|
|
18
15
|
}
|
|
19
|
-
|
|
20
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2NydWNpYmxlL3NyYy9tb2RlbHMvc2VydmljZS1yZWdpc3RyYXRpb24ubW9kZWwudHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHR5cGUgeyBDb25zdHJ1Y3RhYmxlLCBGdW5jdGlvblR5cGUgfSBmcm9tIFwiQGRlY29oZXJlL25leHVzL3JlZmxlY3Rpb25cIjtcbmltcG9ydCB0eXBlIHsgU2NvcGUgfSBmcm9tIFwiLi4vdHlwZXMvdHlwZXMuanNcIjtcbmltcG9ydCB0eXBlIHsgUmVnaXN0cmF0aW9uS2V5IH0gZnJvbSBcIi4vcmVnaXN0cmF0aW9uLWtleS5tb2RlbC5qc1wiO1xuXG5leHBvcnQgY2xhc3MgU2VydmljZVJlZ2lzdHJhdGlvbiB7XG5cdHB1YmxpYyBrZXk6IFJlZ2lzdHJhdGlvbktleTtcblx0cHVibGljIHNjb3BlOiBTY29wZTtcblx0cHVibGljIHR5cGU6IENvbnN0cnVjdGFibGUgfCBGdW5jdGlvblR5cGUgfCBvYmplY3Q7XG5cdHB1YmxpYyBpc0luc3RhbmNlOiBib29sZWFuO1xuXG5cdHB1YmxpYyBjb25zdHJ1Y3Rvcihcblx0XHRrZXk6IFJlZ2lzdHJhdGlvbktleSxcblx0XHR0eXBlOiBDb25zdHJ1Y3RhYmxlIHwgRnVuY3Rpb25UeXBlIHwgb2JqZWN0LFxuXHRcdHNjb3BlOiBTY29wZSxcblx0XHRpc0luc3RhbmNlOiBib29sZWFuLFxuXHQpIHtcblx0XHR0aGlzLmtleSA9IGtleTtcblx0XHR0aGlzLnR5cGUgPSB0eXBlO1xuXHRcdHRoaXMuc2NvcGUgPSBzY29wZTtcblx0XHR0aGlzLmlzSW5zdGFuY2UgPSBpc0luc3RhbmNlO1xuXHR9XG5cblx0cHVibGljIGlzRXF1YWwob3RoZXI6IFNlcnZpY2VSZWdpc3RyYXRpb24pIHtcblx0XHRpZiAob3RoZXIuaXNJbnN0YW5jZSkge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblxuXHRcdHJldHVybiB0aGlzLnR5cGUgPT09IG90aGVyLnR5cGU7XG5cdH1cbn1cbiJdLCJuYW1lcyI6WyJTZXJ2aWNlUmVnaXN0cmF0aW9uIiwia2V5Iiwic2NvcGUiLCJ0eXBlIiwiaXNJbnN0YW5jZSIsImlzRXF1YWwiLCJvdGhlciJdLCJtYXBwaW5ncyI6IkFBSUEsT0FBTyxNQUFNQTtJQUNMQyxJQUFxQjtJQUNyQkMsTUFBYTtJQUNiQyxLQUE0QztJQUM1Q0MsV0FBb0I7SUFFM0IsWUFDQ0gsR0FBb0IsRUFDcEJFLElBQTJDLEVBQzNDRCxLQUFZLEVBQ1pFLFVBQW1CLENBQ2xCO1FBQ0QsSUFBSSxDQUFDSCxHQUFHLEdBQUdBO1FBQ1gsSUFBSSxDQUFDRSxJQUFJLEdBQUdBO1FBQ1osSUFBSSxDQUFDRCxLQUFLLEdBQUdBO1FBQ2IsSUFBSSxDQUFDRSxVQUFVLEdBQUdBO0lBQ25CO0lBRU9DLFFBQVFDLEtBQTBCLEVBQUU7UUFDMUMsSUFBSUEsTUFBTUYsVUFBVSxFQUFFO1lBQ3JCLE9BQU87UUFDUjtRQUVBLE9BQU8sSUFBSSxDQUFDRCxJQUFJLEtBQUtHLE1BQU1ILElBQUk7SUFDaEM7QUFDRCJ9
|